github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/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 // Open opens the REST backend with the given config. 34 func Open(cfg Config, rt http.RoundTripper) (*restBackend, error) { 35 client := &http.Client{Transport: rt} 36 37 sem, err := backend.NewSemaphore(cfg.Connections) 38 if err != nil { 39 return nil, err 40 } 41 42 // use url without trailing slash for layout 43 url := cfg.URL.String() 44 if url[len(url)-1] == '/' { 45 url = url[:len(url)-1] 46 } 47 48 be := &restBackend{ 49 url: cfg.URL, 50 client: client, 51 Layout: &backend.RESTLayout{URL: url, Join: path.Join}, 52 sem: sem, 53 } 54 55 return be, nil 56 } 57 58 // Create creates a new REST on server configured in config. 59 func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 60 be, err := Open(cfg, rt) 61 if err != nil { 62 return nil, err 63 } 64 65 _, err = be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile}) 66 if err == nil { 67 return nil, errors.Fatal("config file already exists") 68 } 69 70 url := *cfg.URL 71 values := url.Query() 72 values.Set("create", "true") 73 url.RawQuery = values.Encode() 74 75 resp, err := be.client.Post(url.String(), "binary/octet-stream", strings.NewReader("")) 76 if err != nil { 77 return nil, err 78 } 79 80 if resp.StatusCode != http.StatusOK { 81 return nil, errors.Fatalf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode) 82 } 83 84 _, err = io.Copy(ioutil.Discard, resp.Body) 85 if err != nil { 86 return nil, err 87 } 88 89 err = resp.Body.Close() 90 if err != nil { 91 return nil, err 92 } 93 94 return be, nil 95 } 96 97 // Location returns this backend's location (the server's URL). 98 func (b *restBackend) Location() string { 99 return b.url.String() 100 } 101 102 // Save stores data in the backend at the handle. 103 func (b *restBackend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) { 104 if err := h.Valid(); err != nil { 105 return err 106 } 107 108 ctx, cancel := context.WithCancel(ctx) 109 defer cancel() 110 111 // make sure that client.Post() cannot close the reader by wrapping it 112 rd = ioutil.NopCloser(rd) 113 114 b.sem.GetToken() 115 resp, err := ctxhttp.Post(ctx, b.client, b.Filename(h), "binary/octet-stream", rd) 116 b.sem.ReleaseToken() 117 118 if resp != nil { 119 defer func() { 120 _, _ = io.Copy(ioutil.Discard, resp.Body) 121 e := resp.Body.Close() 122 123 if err == nil { 124 err = errors.Wrap(e, "Close") 125 } 126 }() 127 } 128 129 if err != nil { 130 return errors.Wrap(err, "client.Post") 131 } 132 133 if resp.StatusCode != 200 { 134 return errors.Errorf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode) 135 } 136 137 return nil 138 } 139 140 // ErrIsNotExist is returned whenever the requested file does not exist on the 141 // server. 142 type ErrIsNotExist struct { 143 restic.Handle 144 } 145 146 func (e ErrIsNotExist) Error() string { 147 return fmt.Sprintf("%v does not exist", e.Handle) 148 } 149 150 // IsNotExist returns true if the error was caused by a non-existing file. 151 func (b *restBackend) IsNotExist(err error) bool { 152 err = errors.Cause(err) 153 _, ok := err.(ErrIsNotExist) 154 return ok 155 } 156 157 // Load returns a reader that yields the contents of the file at h at the 158 // given offset. If length is nonzero, only a portion of the file is 159 // returned. rd must be closed after use. 160 func (b *restBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 161 debug.Log("Load %v, length %v, offset %v", h, length, offset) 162 if err := h.Valid(); err != nil { 163 return nil, err 164 } 165 166 if offset < 0 { 167 return nil, errors.New("offset is negative") 168 } 169 170 if length < 0 { 171 return nil, errors.Errorf("invalid length %d", length) 172 } 173 174 req, err := http.NewRequest("GET", b.Filename(h), nil) 175 if err != nil { 176 return nil, errors.Wrap(err, "http.NewRequest") 177 } 178 179 byteRange := fmt.Sprintf("bytes=%d-", offset) 180 if length > 0 { 181 byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1) 182 } 183 req.Header.Add("Range", byteRange) 184 debug.Log("Load(%v) send range %v", h, byteRange) 185 186 b.sem.GetToken() 187 resp, err := ctxhttp.Do(ctx, b.client, req) 188 b.sem.ReleaseToken() 189 190 if err != nil { 191 if resp != nil { 192 _, _ = io.Copy(ioutil.Discard, resp.Body) 193 _ = resp.Body.Close() 194 } 195 return nil, errors.Wrap(err, "client.Do") 196 } 197 198 if resp.StatusCode == http.StatusNotFound { 199 _ = resp.Body.Close() 200 return nil, ErrIsNotExist{h} 201 } 202 203 if resp.StatusCode != 200 && resp.StatusCode != 206 { 204 _ = resp.Body.Close() 205 return nil, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status) 206 } 207 208 return resp.Body, nil 209 } 210 211 // Stat returns information about a blob. 212 func (b *restBackend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) { 213 if err := h.Valid(); err != nil { 214 return restic.FileInfo{}, err 215 } 216 217 b.sem.GetToken() 218 resp, err := ctxhttp.Head(ctx, b.client, b.Filename(h)) 219 b.sem.ReleaseToken() 220 if err != nil { 221 return restic.FileInfo{}, errors.Wrap(err, "client.Head") 222 } 223 224 _, _ = io.Copy(ioutil.Discard, resp.Body) 225 if err = resp.Body.Close(); err != nil { 226 return restic.FileInfo{}, errors.Wrap(err, "Close") 227 } 228 229 if resp.StatusCode == http.StatusNotFound { 230 _ = resp.Body.Close() 231 return restic.FileInfo{}, ErrIsNotExist{h} 232 } 233 234 if resp.StatusCode != 200 { 235 return restic.FileInfo{}, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status) 236 } 237 238 if resp.ContentLength < 0 { 239 return restic.FileInfo{}, errors.New("negative content length") 240 } 241 242 bi := restic.FileInfo{ 243 Size: resp.ContentLength, 244 } 245 246 return bi, nil 247 } 248 249 // Test returns true if a blob of the given type and name exists in the backend. 250 func (b *restBackend) Test(ctx context.Context, h restic.Handle) (bool, error) { 251 _, err := b.Stat(ctx, h) 252 if err != nil { 253 return false, nil 254 } 255 256 return true, nil 257 } 258 259 // Remove removes the blob with the given name and type. 260 func (b *restBackend) Remove(ctx context.Context, h restic.Handle) error { 261 if err := h.Valid(); err != nil { 262 return err 263 } 264 265 req, err := http.NewRequest("DELETE", b.Filename(h), nil) 266 if err != nil { 267 return errors.Wrap(err, "http.NewRequest") 268 } 269 b.sem.GetToken() 270 resp, err := ctxhttp.Do(ctx, b.client, req) 271 b.sem.ReleaseToken() 272 273 if err != nil { 274 return errors.Wrap(err, "client.Do") 275 } 276 277 if resp.StatusCode == http.StatusNotFound { 278 _ = resp.Body.Close() 279 return ErrIsNotExist{h} 280 } 281 282 if resp.StatusCode != 200 { 283 return errors.Errorf("blob not removed, server response: %v (%v)", resp.Status, resp.StatusCode) 284 } 285 286 _, err = io.Copy(ioutil.Discard, resp.Body) 287 if err != nil { 288 return errors.Wrap(err, "Copy") 289 } 290 291 return errors.Wrap(resp.Body.Close(), "Close") 292 } 293 294 // List returns a channel that yields all names of blobs of type t. A 295 // goroutine is started for this. If the channel done is closed, sending 296 // stops. 297 func (b *restBackend) List(ctx context.Context, t restic.FileType) <-chan string { 298 ch := make(chan string) 299 300 url := b.Dirname(restic.Handle{Type: t}) 301 if !strings.HasSuffix(url, "/") { 302 url += "/" 303 } 304 305 b.sem.GetToken() 306 resp, err := ctxhttp.Get(ctx, b.client, url) 307 b.sem.ReleaseToken() 308 309 if resp != nil { 310 defer func() { 311 _, _ = io.Copy(ioutil.Discard, resp.Body) 312 e := resp.Body.Close() 313 314 if err == nil { 315 err = errors.Wrap(e, "Close") 316 } 317 }() 318 } 319 320 if err != nil { 321 close(ch) 322 return ch 323 } 324 325 dec := json.NewDecoder(resp.Body) 326 var list []string 327 if err = dec.Decode(&list); err != nil { 328 close(ch) 329 return ch 330 } 331 332 go func() { 333 defer close(ch) 334 for _, m := range list { 335 select { 336 case ch <- m: 337 case <-ctx.Done(): 338 return 339 } 340 } 341 }() 342 343 return ch 344 } 345 346 // Close closes all open files. 347 func (b *restBackend) Close() error { 348 // this does not need to do anything, all open files are closed within the 349 // same function. 350 return nil 351 } 352 353 // Remove keys for a specified backend type. 354 func (b *restBackend) removeKeys(ctx context.Context, t restic.FileType) error { 355 for key := range b.List(ctx, restic.DataFile) { 356 err := b.Remove(ctx, restic.Handle{Type: restic.DataFile, Name: key}) 357 if err != nil { 358 return err 359 } 360 } 361 362 return nil 363 } 364 365 // Delete removes all data in the backend. 366 func (b *restBackend) Delete(ctx context.Context) error { 367 alltypes := []restic.FileType{ 368 restic.DataFile, 369 restic.KeyFile, 370 restic.LockFile, 371 restic.SnapshotFile, 372 restic.IndexFile} 373 374 for _, t := range alltypes { 375 err := b.removeKeys(ctx, t) 376 if err != nil { 377 return nil 378 } 379 } 380 381 err := b.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 382 if err != nil && b.IsNotExist(err) { 383 return nil 384 } 385 return err 386 }