rclone/cmd/serve/restic/restic.go
Aleksandar Jankovic 8243ff8bc8 accounting: isolate stats to groups
Introduce stats groups that will isolate accounting for logically
different transferring operations. That way multiple accounting
operations can be done in parallel without interfering with each other
stats.

Using groups is optional. There is dedicated global stats that will be
used by default if no group is specified. This is operating mode for CLI
usage which is just fire and forget operation.

For running rclone as rc http server each request will create it's own
group. Also there is an option to specify your own group.
2019-07-28 14:48:19 +01:00

398 lines
11 KiB
Go

// 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
privateRepos 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.Flags().BoolVar(&privateRepos, "private-repos", false, "users can only access their private repo")
}
// 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
#### Private repositories ####
The "--private-repos" flag can be used to limit users to repositories starting
with a path of "/<username>/".
` + 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)
v := r.Context().Value(httplib.ContextUserKey)
if privateRepos && (v == nil || !strings.HasPrefix(path, "/"+v.(string)+"/")) {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
// 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(r.Context(), 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(r.Context(), 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(r.Context(), s.f, remote, r.Body, r.ContentLength, time.Now())
if err != nil {
accounting.Stats(r.Context()).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(r.Context(), 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(r.Context()); 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(r.Context(), 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(r.Context(), 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(r.Context(), dirRemote)
if err != nil {
fs.Errorf(dirRemote, "Create repo failed to Mkdir: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
}