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 }