github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/cmd/serve/restic/restic.go (about)

     1  // Package restic serves a remote suitable for use with restic
     2  
     3  // +build go1.9
     4  
     5  package restic
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/ncw/rclone/cmd"
    18  	"github.com/ncw/rclone/cmd/serve/httplib"
    19  	"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
    20  	"github.com/ncw/rclone/cmd/serve/httplib/serve"
    21  	"github.com/ncw/rclone/fs"
    22  	"github.com/ncw/rclone/fs/accounting"
    23  	"github.com/ncw/rclone/fs/fserrors"
    24  	"github.com/ncw/rclone/fs/operations"
    25  	"github.com/ncw/rclone/fs/walk"
    26  	"github.com/spf13/cobra"
    27  	"golang.org/x/crypto/ssh/terminal"
    28  	"golang.org/x/net/http2"
    29  )
    30  
    31  var (
    32  	stdio        bool
    33  	appendOnly   bool
    34  	privateRepos bool
    35  )
    36  
    37  func init() {
    38  	httpflags.AddFlags(Command.Flags())
    39  	Command.Flags().BoolVar(&stdio, "stdio", false, "run an HTTP2 server on stdin/stdout")
    40  	Command.Flags().BoolVar(&appendOnly, "append-only", false, "disallow deletion of repository data")
    41  	Command.Flags().BoolVar(&privateRepos, "private-repos", false, "users can only access their private repo")
    42  }
    43  
    44  // Command definition for cobra
    45  var Command = &cobra.Command{
    46  	Use:   "restic remote:path",
    47  	Short: `Serve the remote for restic's REST API.`,
    48  	Long: `rclone serve restic implements restic's REST backend API
    49  over HTTP.  This allows restic to use rclone as a data storage
    50  mechanism for cloud providers that restic does not support directly.
    51  
    52  [Restic](https://restic.net/) is a command line program for doing
    53  backups.
    54  
    55  The server will log errors.  Use -v to see access logs.
    56  
    57  --bwlimit will be respected for file transfers.  Use --stats to
    58  control the stats printing.
    59  
    60  ### Setting up rclone for use by restic ###
    61  
    62  First [set up a remote for your chosen cloud provider](/docs/#configure).
    63  
    64  Once you have set up the remote, check it is working with, for example
    65  "rclone lsd remote:".  You may have called the remote something other
    66  than "remote:" - just substitute whatever you called it in the
    67  following instructions.
    68  
    69  Now start the rclone restic server
    70  
    71      rclone serve restic -v remote:backup
    72  
    73  Where you can replace "backup" in the above by whatever path in the
    74  remote you wish to use.
    75  
    76  By default this will serve on "localhost:8080" you can change this
    77  with use of the "--addr" flag.
    78  
    79  You might wish to start this server on boot.
    80  
    81  ### Setting up restic to use rclone ###
    82  
    83  Now you can [follow the restic
    84  instructions](http://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server)
    85  on setting up restic.
    86  
    87  Note that you will need restic 0.8.2 or later to interoperate with
    88  rclone.
    89  
    90  For the example above you will want to use "http://localhost:8080/" as
    91  the URL for the REST server.
    92  
    93  For example:
    94  
    95      $ export RESTIC_REPOSITORY=rest:http://localhost:8080/
    96      $ export RESTIC_PASSWORD=yourpassword
    97      $ restic init
    98      created restic backend 8b1a4b56ae at rest:http://localhost:8080/
    99  
   100      Please note that knowledge of your password is required to access
   101      the repository. Losing your password means that your data is
   102      irrecoverably lost.
   103      $ restic backup /path/to/files/to/backup
   104      scan [/path/to/files/to/backup]
   105      scanned 189 directories, 312 files in 0:00
   106      [0:00] 100.00%  38.128 MiB / 38.128 MiB  501 / 501 items  0 errors  ETA 0:00
   107      duration: 0:00
   108      snapshot 45c8fdd8 saved
   109  
   110  #### Multiple repositories ####
   111  
   112  Note that you can use the endpoint to host multiple repositories.  Do
   113  this by adding a directory name or path after the URL.  Note that
   114  these **must** end with /.  Eg
   115  
   116      $ export RESTIC_REPOSITORY=rest:http://localhost:8080/user1repo/
   117      # backup user1 stuff
   118      $ export RESTIC_REPOSITORY=rest:http://localhost:8080/user2repo/
   119      # backup user2 stuff
   120  
   121  #### Private repositories ####
   122  
   123  The "--private-repos" flag can be used to limit users to repositories starting
   124  with a path of "/<username>/".
   125  ` + httplib.Help,
   126  	Run: func(command *cobra.Command, args []string) {
   127  		cmd.CheckArgs(1, 1, command, args)
   128  		f := cmd.NewFsSrc(args)
   129  		cmd.Run(false, true, command, func() error {
   130  			s := newServer(f, &httpflags.Opt)
   131  			if stdio {
   132  				if terminal.IsTerminal(int(os.Stdout.Fd())) {
   133  					return errors.New("Refusing to run HTTP2 server directly on a terminal, please let restic start rclone")
   134  				}
   135  
   136  				conn := &StdioConn{
   137  					stdin:  os.Stdin,
   138  					stdout: os.Stdout,
   139  				}
   140  
   141  				httpSrv := &http2.Server{}
   142  				opts := &http2.ServeConnOpts{
   143  					Handler: http.HandlerFunc(s.handler),
   144  				}
   145  				httpSrv.ServeConn(conn, opts)
   146  				return nil
   147  			}
   148  			err := s.Serve()
   149  			if err != nil {
   150  				return err
   151  			}
   152  			s.Wait()
   153  			return nil
   154  		})
   155  	},
   156  }
   157  
   158  const (
   159  	resticAPIV2 = "application/vnd.x.restic.rest.v2"
   160  )
   161  
   162  // server contains everything to run the server
   163  type server struct {
   164  	*httplib.Server
   165  	f fs.Fs
   166  }
   167  
   168  func newServer(f fs.Fs, opt *httplib.Options) *server {
   169  	mux := http.NewServeMux()
   170  	s := &server{
   171  		Server: httplib.NewServer(mux, opt),
   172  		f:      f,
   173  	}
   174  	mux.HandleFunc("/", s.handler)
   175  	return s
   176  }
   177  
   178  // Serve runs the http server in the background.
   179  //
   180  // Use s.Close() and s.Wait() to shutdown server
   181  func (s *server) Serve() error {
   182  	err := s.Server.Serve()
   183  	if err != nil {
   184  		return err
   185  	}
   186  	fs.Logf(s.f, "Serving restic REST API on %s", s.URL())
   187  	return nil
   188  }
   189  
   190  var matchData = regexp.MustCompile("(?:^|/)data/([^/]{2,})$")
   191  
   192  // Makes a remote from a URL path.  This implements the backend layout
   193  // required by restic.
   194  func makeRemote(path string) string {
   195  	path = strings.Trim(path, "/")
   196  	parts := matchData.FindStringSubmatch(path)
   197  	// if no data directory, layout is flat
   198  	if parts == nil {
   199  		return path
   200  	}
   201  	// otherwise map
   202  	// data/2159dd48 to
   203  	// data/21/2159dd48
   204  	fileName := parts[1]
   205  	prefix := path[:len(path)-len(fileName)]
   206  	return prefix + fileName[:2] + "/" + fileName
   207  }
   208  
   209  // handler reads incoming requests and dispatches them
   210  func (s *server) handler(w http.ResponseWriter, r *http.Request) {
   211  	w.Header().Set("Accept-Ranges", "bytes")
   212  	w.Header().Set("Server", "rclone/"+fs.Version)
   213  
   214  	path := r.URL.Path
   215  	remote := makeRemote(path)
   216  	fs.Debugf(s.f, "%s %s", r.Method, path)
   217  
   218  	v := r.Context().Value(httplib.ContextUserKey)
   219  	if privateRepos && (v == nil || !strings.HasPrefix(path, "/"+v.(string)+"/")) {
   220  		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
   221  		return
   222  	}
   223  
   224  	// Dispatch on path then method
   225  	if strings.HasSuffix(path, "/") {
   226  		switch r.Method {
   227  		case "GET":
   228  			s.listObjects(w, r, remote)
   229  		case "POST":
   230  			s.createRepo(w, r, remote)
   231  		default:
   232  			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
   233  		}
   234  	} else {
   235  		switch r.Method {
   236  		case "GET", "HEAD":
   237  			s.serveObject(w, r, remote)
   238  		case "POST":
   239  			s.postObject(w, r, remote)
   240  		case "DELETE":
   241  			s.deleteObject(w, r, remote)
   242  		default:
   243  			http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
   244  		}
   245  	}
   246  }
   247  
   248  // get the remote
   249  func (s *server) serveObject(w http.ResponseWriter, r *http.Request, remote string) {
   250  	o, err := s.f.NewObject(r.Context(), remote)
   251  	if err != nil {
   252  		fs.Debugf(remote, "%s request error: %v", r.Method, err)
   253  		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   254  		return
   255  	}
   256  	serve.Object(w, r, o)
   257  }
   258  
   259  // postObject posts an object to the repository
   260  func (s *server) postObject(w http.ResponseWriter, r *http.Request, remote string) {
   261  	if appendOnly {
   262  		// make sure the file does not exist yet
   263  		_, err := s.f.NewObject(r.Context(), remote)
   264  		if err == nil {
   265  			fs.Errorf(remote, "Post request: file already exists, refusing to overwrite in append-only mode")
   266  			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
   267  
   268  			return
   269  		}
   270  	}
   271  
   272  	_, err := operations.RcatSize(r.Context(), s.f, remote, r.Body, r.ContentLength, time.Now())
   273  	if err != nil {
   274  		accounting.Stats.Error(err)
   275  		fs.Errorf(remote, "Post request rcat error: %v", err)
   276  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   277  
   278  		return
   279  	}
   280  }
   281  
   282  // delete the remote
   283  func (s *server) deleteObject(w http.ResponseWriter, r *http.Request, remote string) {
   284  	if appendOnly {
   285  		parts := strings.Split(r.URL.Path, "/")
   286  
   287  		// if path doesn't end in "/locks/:name", disallow the operation
   288  		if len(parts) < 2 || parts[len(parts)-2] != "locks" {
   289  			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
   290  			return
   291  		}
   292  	}
   293  
   294  	o, err := s.f.NewObject(r.Context(), remote)
   295  	if err != nil {
   296  		fs.Debugf(remote, "Delete request error: %v", err)
   297  		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   298  		return
   299  	}
   300  
   301  	if err := o.Remove(r.Context()); err != nil {
   302  		fs.Errorf(remote, "Delete request remove error: %v", err)
   303  		if err == fs.ErrorObjectNotFound {
   304  			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   305  		} else {
   306  			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   307  		}
   308  		return
   309  	}
   310  }
   311  
   312  // listItem is an element returned for the restic v2 list response
   313  type listItem struct {
   314  	Name string `json:"name"`
   315  	Size int64  `json:"size"`
   316  }
   317  
   318  // return type for list
   319  type listItems []listItem
   320  
   321  // add a DirEntry to the listItems
   322  func (ls *listItems) add(entry fs.DirEntry) {
   323  	if o, ok := entry.(fs.Object); ok {
   324  		*ls = append(*ls, listItem{
   325  			Name: path.Base(o.Remote()),
   326  			Size: o.Size(),
   327  		})
   328  	}
   329  }
   330  
   331  // listObjects lists all Objects of a given type in an arbitrary order.
   332  func (s *server) listObjects(w http.ResponseWriter, r *http.Request, remote string) {
   333  	fs.Debugf(remote, "list request")
   334  
   335  	if r.Header.Get("Accept") != resticAPIV2 {
   336  		fs.Errorf(remote, "Restic v2 API required")
   337  		http.Error(w, "Restic v2 API required", http.StatusBadRequest)
   338  		return
   339  	}
   340  
   341  	// make sure an empty list is returned, and not a 'nil' value
   342  	ls := listItems{}
   343  
   344  	// if remote supports ListR use that directly, otherwise use recursive Walk
   345  	err := walk.ListR(r.Context(), s.f, remote, true, -1, walk.ListObjects, func(entries fs.DirEntries) error {
   346  		for _, entry := range entries {
   347  			ls.add(entry)
   348  		}
   349  		return nil
   350  	})
   351  	if err != nil {
   352  		_, err = fserrors.Cause(err)
   353  		if err != fs.ErrorDirNotFound {
   354  			fs.Errorf(remote, "list failed: %#v %T", err, err)
   355  			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   356  			return
   357  		}
   358  	}
   359  
   360  	w.Header().Set("Content-Type", "application/vnd.x.restic.rest.v2")
   361  	enc := json.NewEncoder(w)
   362  	err = enc.Encode(ls)
   363  	if err != nil {
   364  		fs.Errorf(remote, "failed to write list: %v", err)
   365  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   366  		return
   367  	}
   368  }
   369  
   370  // createRepo creates repository directories.
   371  //
   372  // We don't bother creating the data dirs as rclone will create them on the fly
   373  func (s *server) createRepo(w http.ResponseWriter, r *http.Request, remote string) {
   374  	fs.Infof(remote, "Creating repository")
   375  
   376  	if r.URL.Query().Get("create") != "true" {
   377  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   378  		return
   379  	}
   380  
   381  	err := s.f.Mkdir(r.Context(), remote)
   382  	if err != nil {
   383  		fs.Errorf(remote, "Create repo failed to Mkdir: %v", err)
   384  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   385  		return
   386  	}
   387  
   388  	for _, name := range []string{"data", "index", "keys", "locks", "snapshots"} {
   389  		dirRemote := path.Join(remote, name)
   390  		err := s.f.Mkdir(r.Context(), dirRemote)
   391  		if err != nil {
   392  			fs.Errorf(dirRemote, "Create repo failed to Mkdir: %v", err)
   393  			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   394  			return
   395  		}
   396  	}
   397  }