github.com/mckael/restic@v0.8.3/internal/backend/rest/rest.go (about) 1 package rest 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 "path" 12 "strings" 13 14 "golang.org/x/net/context/ctxhttp" 15 16 "github.com/restic/restic/internal/debug" 17 "github.com/restic/restic/internal/errors" 18 "github.com/restic/restic/internal/restic" 19 20 "github.com/restic/restic/internal/backend" 21 ) 22 23 // make sure the rest backend implements restic.Backend 24 var _ restic.Backend = &restBackend{} 25 26 type restBackend struct { 27 url *url.URL 28 sem *backend.Semaphore 29 client *http.Client 30 backend.Layout 31 } 32 33 const ( 34 contentTypeV1 = "application/vnd.x.restic.rest.v1" 35 contentTypeV2 = "application/vnd.x.restic.rest.v2" 36 ) 37 38 // Open opens the REST backend with the given config. 39 func Open(cfg Config, rt http.RoundTripper) (*restBackend, error) { 40 client := &http.Client{Transport: rt} 41 42 sem, err := backend.NewSemaphore(cfg.Connections) 43 if err != nil { 44 return nil, err 45 } 46 47 // use url without trailing slash for layout 48 url := cfg.URL.String() 49 if url[len(url)-1] == '/' { 50 url = url[:len(url)-1] 51 } 52 53 be := &restBackend{ 54 url: cfg.URL, 55 client: client, 56 Layout: &backend.RESTLayout{URL: url, Join: path.Join}, 57 sem: sem, 58 } 59 60 return be, nil 61 } 62 63 // Create creates a new REST on server configured in config. 64 func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 65 be, err := Open(cfg, rt) 66 if err != nil { 67 return nil, err 68 } 69 70 _, err = be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile}) 71 if err == nil { 72 return nil, errors.Fatal("config file already exists") 73 } 74 75 url := *cfg.URL 76 values := url.Query() 77 values.Set("create", "true") 78 url.RawQuery = values.Encode() 79 80 resp, err := be.client.Post(url.String(), "binary/octet-stream", strings.NewReader("")) 81 if err != nil { 82 return nil, err 83 } 84 85 if resp.StatusCode != http.StatusOK { 86 return nil, errors.Fatalf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode) 87 } 88 89 _, err = io.Copy(ioutil.Discard, resp.Body) 90 if err != nil { 91 return nil, err 92 } 93 94 err = resp.Body.Close() 95 if err != nil { 96 return nil, err 97 } 98 99 return be, nil 100 } 101 102 // Location returns this backend's location (the server's URL). 103 func (b *restBackend) Location() string { 104 return b.url.String() 105 } 106 107 // Save stores data in the backend at the handle. 108 func (b *restBackend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) { 109 if err := h.Valid(); err != nil { 110 return err 111 } 112 113 ctx, cancel := context.WithCancel(ctx) 114 defer cancel() 115 116 // make sure that client.Post() cannot close the reader by wrapping it 117 rd = ioutil.NopCloser(rd) 118 119 req, err := http.NewRequest(http.MethodPost, b.Filename(h), rd) 120 if err != nil { 121 return errors.Wrap(err, "NewRequest") 122 } 123 req.Header.Set("Content-Type", "application/octet-stream") 124 req.Header.Set("Accept", contentTypeV2) 125 126 b.sem.GetToken() 127 resp, err := ctxhttp.Do(ctx, b.client, req) 128 b.sem.ReleaseToken() 129 130 if resp != nil { 131 defer func() { 132 _, _ = io.Copy(ioutil.Discard, resp.Body) 133 e := resp.Body.Close() 134 135 if err == nil { 136 err = errors.Wrap(e, "Close") 137 } 138 }() 139 } 140 141 if err != nil { 142 return errors.Wrap(err, "client.Post") 143 } 144 145 if resp.StatusCode != 200 { 146 return errors.Errorf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode) 147 } 148 149 return nil 150 } 151 152 // ErrIsNotExist is returned whenever the requested file does not exist on the 153 // server. 154 type ErrIsNotExist struct { 155 restic.Handle 156 } 157 158 func (e ErrIsNotExist) Error() string { 159 return fmt.Sprintf("%v does not exist", e.Handle) 160 } 161 162 // IsNotExist returns true if the error was caused by a non-existing file. 163 func (b *restBackend) IsNotExist(err error) bool { 164 err = errors.Cause(err) 165 _, ok := err.(ErrIsNotExist) 166 return ok 167 } 168 169 // Load runs fn with a reader that yields the contents of the file at h at the 170 // given offset. 171 func (b *restBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error { 172 return backend.DefaultLoad(ctx, h, length, offset, b.openReader, fn) 173 } 174 175 func (b *restBackend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 176 debug.Log("Load %v, length %v, offset %v", h, length, offset) 177 if err := h.Valid(); err != nil { 178 return nil, err 179 } 180 181 if offset < 0 { 182 return nil, errors.New("offset is negative") 183 } 184 185 if length < 0 { 186 return nil, errors.Errorf("invalid length %d", length) 187 } 188 189 req, err := http.NewRequest("GET", b.Filename(h), nil) 190 if err != nil { 191 return nil, errors.Wrap(err, "http.NewRequest") 192 } 193 194 byteRange := fmt.Sprintf("bytes=%d-", offset) 195 if length > 0 { 196 byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1) 197 } 198 req.Header.Set("Range", byteRange) 199 req.Header.Set("Accept", contentTypeV2) 200 debug.Log("Load(%v) send range %v", h, byteRange) 201 202 b.sem.GetToken() 203 resp, err := ctxhttp.Do(ctx, b.client, req) 204 b.sem.ReleaseToken() 205 206 if err != nil { 207 if resp != nil { 208 _, _ = io.Copy(ioutil.Discard, resp.Body) 209 _ = resp.Body.Close() 210 } 211 return nil, errors.Wrap(err, "client.Do") 212 } 213 214 if resp.StatusCode == http.StatusNotFound { 215 _ = resp.Body.Close() 216 return nil, ErrIsNotExist{h} 217 } 218 219 if resp.StatusCode != 200 && resp.StatusCode != 206 { 220 _ = resp.Body.Close() 221 return nil, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status) 222 } 223 224 return resp.Body, nil 225 } 226 227 // Stat returns information about a blob. 228 func (b *restBackend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) { 229 if err := h.Valid(); err != nil { 230 return restic.FileInfo{}, err 231 } 232 233 req, err := http.NewRequest(http.MethodHead, b.Filename(h), nil) 234 if err != nil { 235 return restic.FileInfo{}, errors.Wrap(err, "NewRequest") 236 } 237 req.Header.Set("Accept", contentTypeV2) 238 239 b.sem.GetToken() 240 resp, err := ctxhttp.Do(ctx, b.client, req) 241 b.sem.ReleaseToken() 242 if err != nil { 243 return restic.FileInfo{}, errors.Wrap(err, "client.Head") 244 } 245 246 _, _ = io.Copy(ioutil.Discard, resp.Body) 247 if err = resp.Body.Close(); err != nil { 248 return restic.FileInfo{}, errors.Wrap(err, "Close") 249 } 250 251 if resp.StatusCode == http.StatusNotFound { 252 _ = resp.Body.Close() 253 return restic.FileInfo{}, ErrIsNotExist{h} 254 } 255 256 if resp.StatusCode != 200 { 257 return restic.FileInfo{}, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status) 258 } 259 260 if resp.ContentLength < 0 { 261 return restic.FileInfo{}, errors.New("negative content length") 262 } 263 264 bi := restic.FileInfo{ 265 Size: resp.ContentLength, 266 Name: h.Name, 267 } 268 269 return bi, nil 270 } 271 272 // Test returns true if a blob of the given type and name exists in the backend. 273 func (b *restBackend) Test(ctx context.Context, h restic.Handle) (bool, error) { 274 _, err := b.Stat(ctx, h) 275 if err != nil { 276 return false, nil 277 } 278 279 return true, nil 280 } 281 282 // Remove removes the blob with the given name and type. 283 func (b *restBackend) Remove(ctx context.Context, h restic.Handle) error { 284 if err := h.Valid(); err != nil { 285 return err 286 } 287 288 req, err := http.NewRequest("DELETE", b.Filename(h), nil) 289 if err != nil { 290 return errors.Wrap(err, "http.NewRequest") 291 } 292 req.Header.Set("Accept", contentTypeV2) 293 294 b.sem.GetToken() 295 resp, err := ctxhttp.Do(ctx, b.client, req) 296 b.sem.ReleaseToken() 297 298 if err != nil { 299 return errors.Wrap(err, "client.Do") 300 } 301 302 if resp.StatusCode == http.StatusNotFound { 303 _ = resp.Body.Close() 304 return ErrIsNotExist{h} 305 } 306 307 if resp.StatusCode != 200 { 308 return errors.Errorf("blob not removed, server response: %v (%v)", resp.Status, resp.StatusCode) 309 } 310 311 _, err = io.Copy(ioutil.Discard, resp.Body) 312 if err != nil { 313 return errors.Wrap(err, "Copy") 314 } 315 316 return errors.Wrap(resp.Body.Close(), "Close") 317 } 318 319 // List runs fn for each file in the backend which has the type t. When an 320 // error occurs (or fn returns an error), List stops and returns it. 321 func (b *restBackend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error { 322 url := b.Dirname(restic.Handle{Type: t}) 323 if !strings.HasSuffix(url, "/") { 324 url += "/" 325 } 326 327 req, err := http.NewRequest(http.MethodGet, url, nil) 328 if err != nil { 329 return errors.Wrap(err, "NewRequest") 330 } 331 req.Header.Set("Accept", contentTypeV2) 332 333 b.sem.GetToken() 334 resp, err := ctxhttp.Do(ctx, b.client, req) 335 b.sem.ReleaseToken() 336 337 if err != nil { 338 return errors.Wrap(err, "Get") 339 } 340 341 if resp.Header.Get("Content-Type") == contentTypeV2 { 342 return b.listv2(ctx, t, resp, fn) 343 } 344 345 return b.listv1(ctx, t, resp, fn) 346 } 347 348 // listv1 uses the REST protocol v1, where a list HTTP request (e.g. `GET 349 // /data/`) only returns the names of the files, so we need to issue an HTTP 350 // HEAD request for each file. 351 func (b *restBackend) listv1(ctx context.Context, t restic.FileType, resp *http.Response, fn func(restic.FileInfo) error) error { 352 debug.Log("parsing API v1 response") 353 dec := json.NewDecoder(resp.Body) 354 var list []string 355 if err := dec.Decode(&list); err != nil { 356 return errors.Wrap(err, "Decode") 357 } 358 359 for _, m := range list { 360 fi, err := b.Stat(ctx, restic.Handle{Name: m, Type: t}) 361 if err != nil { 362 return err 363 } 364 365 if ctx.Err() != nil { 366 return ctx.Err() 367 } 368 369 fi.Name = m 370 err = fn(fi) 371 if err != nil { 372 return err 373 } 374 375 if ctx.Err() != nil { 376 return ctx.Err() 377 } 378 } 379 380 return ctx.Err() 381 } 382 383 // listv2 uses the REST protocol v2, where a list HTTP request (e.g. `GET 384 // /data/`) returns the names and sizes of all files. 385 func (b *restBackend) listv2(ctx context.Context, t restic.FileType, resp *http.Response, fn func(restic.FileInfo) error) error { 386 debug.Log("parsing API v2 response") 387 dec := json.NewDecoder(resp.Body) 388 389 var list []struct { 390 Name string `json:"name"` 391 Size int64 `json:"size"` 392 } 393 if err := dec.Decode(&list); err != nil { 394 return errors.Wrap(err, "Decode") 395 } 396 397 for _, item := range list { 398 if ctx.Err() != nil { 399 return ctx.Err() 400 } 401 402 fi := restic.FileInfo{ 403 Name: item.Name, 404 Size: item.Size, 405 } 406 407 err := fn(fi) 408 if err != nil { 409 return err 410 } 411 412 if ctx.Err() != nil { 413 return ctx.Err() 414 } 415 } 416 417 return ctx.Err() 418 } 419 420 // Close closes all open files. 421 func (b *restBackend) Close() error { 422 // this does not need to do anything, all open files are closed within the 423 // same function. 424 return nil 425 } 426 427 // Remove keys for a specified backend type. 428 func (b *restBackend) removeKeys(ctx context.Context, t restic.FileType) error { 429 return b.List(ctx, t, func(fi restic.FileInfo) error { 430 return b.Remove(ctx, restic.Handle{Type: t, Name: fi.Name}) 431 }) 432 } 433 434 // Delete removes all data in the backend. 435 func (b *restBackend) Delete(ctx context.Context) error { 436 alltypes := []restic.FileType{ 437 restic.DataFile, 438 restic.KeyFile, 439 restic.LockFile, 440 restic.SnapshotFile, 441 restic.IndexFile} 442 443 for _, t := range alltypes { 444 err := b.removeKeys(ctx, t) 445 if err != nil { 446 return nil 447 } 448 } 449 450 err := b.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 451 if err != nil && b.IsNotExist(err) { 452 return nil 453 } 454 return err 455 }