// Package restic serves a remote suitable for use with restic

// +build go1.9

package restic

import (
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"path"
	"regexp"
	"strings"
	"time"

	"github.com/ncw/rclone/cmd"
	"github.com/ncw/rclone/cmd/serve/httplib"
	"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
	"github.com/ncw/rclone/cmd/serve/httplib/serve"
	"github.com/ncw/rclone/fs"
	"github.com/ncw/rclone/fs/accounting"
	"github.com/ncw/rclone/fs/fserrors"
	"github.com/ncw/rclone/fs/operations"
	"github.com/ncw/rclone/fs/walk"
	"github.com/spf13/cobra"
	"golang.org/x/crypto/ssh/terminal"
	"golang.org/x/net/http2"
)

var (
	stdio      bool
	appendOnly bool
)

func init() {
	httpflags.AddFlags(Command.Flags())
	Command.Flags().BoolVar(&stdio, "stdio", false, "run an HTTP2 server on stdin/stdout")
	Command.Flags().BoolVar(&appendOnly, "append-only", false, "disallow deletion of repository data")
}

// Command definition for cobra
var Command = &cobra.Command{
	Use:   "restic remote:path",
	Short: `Serve the remote for restic's REST API.`,
	Long: `rclone serve restic implements restic's REST backend API
over HTTP.  This allows restic to use rclone as a data storage
mechanism for cloud providers that restic does not support directly.

[Restic](https://restic.net/) is a command line program for doing
backups.

The server will log errors.  Use -v to see access logs.

--bwlimit will be respected for file transfers.  Use --stats to
control the stats printing.

### Setting up rclone for use by restic ###

First [set up a remote for your chosen cloud provider](/docs/#configure).

Once you have set up the remote, check it is working with, for example
"rclone lsd remote:".  You may have called the remote something other
than "remote:" - just substitute whatever you called it in the
following instructions.

Now start the rclone restic server

    rclone serve restic -v remote:backup

Where you can replace "backup" in the above by whatever path in the
remote you wish to use.

By default this will serve on "localhost:8080" you can change this
with use of the "--addr" flag.

You might wish to start this server on boot.

### Setting up restic to use rclone ###

Now you can [follow the restic
instructions](http://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server)
on setting up restic.

Note that you will need restic 0.8.2 or later to interoperate with
rclone.

For the example above you will want to use "http://localhost:8080/" as
the URL for the REST server.

For example:

    $ export RESTIC_REPOSITORY=rest:http://localhost:8080/
    $ export RESTIC_PASSWORD=yourpassword
    $ restic init
    created restic backend 8b1a4b56ae at rest:http://localhost:8080/
    
    Please note that knowledge of your password is required to access
    the repository. Losing your password means that your data is
    irrecoverably lost.
    $ restic backup /path/to/files/to/backup
    scan [/path/to/files/to/backup]
    scanned 189 directories, 312 files in 0:00
    [0:00] 100.00%  38.128 MiB / 38.128 MiB  501 / 501 items  0 errors  ETA 0:00 
    duration: 0:00
    snapshot 45c8fdd8 saved

#### Multiple repositories ####

Note that you can use the endpoint to host multiple repositories.  Do
this by adding a directory name or path after the URL.  Note that
these **must** end with /.  Eg

    $ export RESTIC_REPOSITORY=rest:http://localhost:8080/user1repo/
    # backup user1 stuff
    $ export RESTIC_REPOSITORY=rest:http://localhost:8080/user2repo/
    # backup user2 stuff

` + httplib.Help,
	Run: func(command *cobra.Command, args []string) {
		cmd.CheckArgs(1, 1, command, args)
		f := cmd.NewFsSrc(args)
		cmd.Run(false, true, command, func() error {
			s := newServer(f, &httpflags.Opt)
			if stdio {
				if terminal.IsTerminal(int(os.Stdout.Fd())) {
					return errors.New("Refusing to run HTTP2 server directly on a terminal, please let restic start rclone")
				}

				conn := &StdioConn{
					stdin:  os.Stdin,
					stdout: os.Stdout,
				}

				httpSrv := &http2.Server{}
				opts := &http2.ServeConnOpts{
					Handler: http.HandlerFunc(s.handler),
				}
				httpSrv.ServeConn(conn, opts)
				return nil
			}
			err := s.Serve()
			if err != nil {
				return err
			}
			s.Wait()
			return nil
		})
	},
}

const (
	resticAPIV2 = "application/vnd.x.restic.rest.v2"
)

// server contains everything to run the server
type server struct {
	*httplib.Server
	f fs.Fs
}

func newServer(f fs.Fs, opt *httplib.Options) *server {
	mux := http.NewServeMux()
	s := &server{
		Server: httplib.NewServer(mux, opt),
		f:      f,
	}
	mux.HandleFunc("/", s.handler)
	return s
}

// Serve runs the http server in the background.
//
// Use s.Close() and s.Wait() to shutdown server
func (s *server) Serve() error {
	err := s.Server.Serve()
	if err != nil {
		return err
	}
	fs.Logf(s.f, "Serving restic REST API on %s", s.URL())
	return nil
}

var matchData = regexp.MustCompile("(?:^|/)data/([^/]{2,})$")

// Makes a remote from a URL path.  This implements the backend layout
// required by restic.
func makeRemote(path string) string {
	path = strings.Trim(path, "/")
	parts := matchData.FindStringSubmatch(path)
	// if no data directory, layout is flat
	if parts == nil {
		return path
	}
	// otherwise map
	// data/2159dd48 to
	// data/21/2159dd48
	fileName := parts[1]
	prefix := path[:len(path)-len(fileName)]
	return prefix + fileName[:2] + "/" + fileName
}

// handler reads incoming requests and dispatches them
func (s *server) handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Accept-Ranges", "bytes")
	w.Header().Set("Server", "rclone/"+fs.Version)

	path := r.URL.Path
	remote := makeRemote(path)
	fs.Debugf(s.f, "%s %s", r.Method, path)

	// Dispatch on path then method
	if strings.HasSuffix(path, "/") {
		switch r.Method {
		case "GET":
			s.listObjects(w, r, remote)
		case "POST":
			s.createRepo(w, r, remote)
		default:
			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		}
	} else {
		switch r.Method {
		case "GET", "HEAD":
			s.serveObject(w, r, remote)
		case "POST":
			s.postObject(w, r, remote)
		case "DELETE":
			s.deleteObject(w, r, remote)
		default:
			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		}
	}
}

// get the remote
func (s *server) serveObject(w http.ResponseWriter, r *http.Request, remote string) {
	o, err := s.f.NewObject(remote)
	if err != nil {
		fs.Debugf(remote, "%s request error: %v", r.Method, err)
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}
	serve.Object(w, r, o)
}

// postObject posts an object to the repository
func (s *server) postObject(w http.ResponseWriter, r *http.Request, remote string) {
	if appendOnly {
		// make sure the file does not exist yet
		_, err := s.f.NewObject(remote)
		if err == nil {
			fs.Errorf(remote, "Post request: file already exists, refusing to overwrite in append-only mode")
			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)

			return
		}
	}

	_, err := operations.RcatSize(s.f, remote, r.Body, r.ContentLength, time.Now())
	if err != nil {
		accounting.Stats.Error(err)
		fs.Errorf(remote, "Post request rcat error: %v", err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

		return
	}
}

// delete the remote
func (s *server) deleteObject(w http.ResponseWriter, r *http.Request, remote string) {
	if appendOnly {
		parts := strings.Split(r.URL.Path, "/")

		// if path doesn't end in "/locks/:name", disallow the operation
		if len(parts) < 2 || parts[len(parts)-2] != "locks" {
			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
			return
		}
	}

	o, err := s.f.NewObject(remote)
	if err != nil {
		fs.Debugf(remote, "Delete request error: %v", err)
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	if err := o.Remove(); err != nil {
		fs.Errorf(remote, "Delete request remove error: %v", err)
		if err == fs.ErrorObjectNotFound {
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		} else {
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}
		return
	}
}

// listItem is an element returned for the restic v2 list response
type listItem struct {
	Name string `json:"name"`
	Size int64  `json:"size"`
}

// return type for list
type listItems []listItem

// add a DirEntry to the listItems
func (ls *listItems) add(entry fs.DirEntry) {
	if o, ok := entry.(fs.Object); ok {
		*ls = append(*ls, listItem{
			Name: path.Base(o.Remote()),
			Size: o.Size(),
		})
	}
}

// listObjects lists all Objects of a given type in an arbitrary order.
func (s *server) listObjects(w http.ResponseWriter, r *http.Request, remote string) {
	fs.Debugf(remote, "list request")

	if r.Header.Get("Accept") != resticAPIV2 {
		fs.Errorf(remote, "Restic v2 API required")
		http.Error(w, "Restic v2 API required", http.StatusBadRequest)
		return
	}

	// make sure an empty list is returned, and not a 'nil' value
	ls := listItems{}

	// if remote supports ListR use that directly, otherwise use recursive Walk
	err := walk.ListR(s.f, remote, true, -1, walk.ListObjects, func(entries fs.DirEntries) error {
		for _, entry := range entries {
			ls.add(entry)
		}
		return nil
	})
	if err != nil {
		_, err = fserrors.Cause(err)
		if err != fs.ErrorDirNotFound {
			fs.Errorf(remote, "list failed: %#v %T", err, err)
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
	}

	w.Header().Set("Content-Type", "application/vnd.x.restic.rest.v2")
	enc := json.NewEncoder(w)
	err = enc.Encode(ls)
	if err != nil {
		fs.Errorf(remote, "failed to write list: %v", err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
}

// createRepo creates repository directories.
//
// We don't bother creating the data dirs as rclone will create them on the fly
func (s *server) createRepo(w http.ResponseWriter, r *http.Request, remote string) {
	fs.Infof(remote, "Creating repository")

	if r.URL.Query().Get("create") != "true" {
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	err := s.f.Mkdir(remote)
	if err != nil {
		fs.Errorf(remote, "Create repo failed to Mkdir: %v", err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	for _, name := range []string{"data", "index", "keys", "locks", "snapshots"} {
		dirRemote := path.Join(remote, name)
		err := s.f.Mkdir(dirRemote)
		if err != nil {
			fs.Errorf(dirRemote, "Create repo failed to Mkdir: %v", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
	}
}
