github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/internal/backend/s3/s3.go (about) 1 package s3 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path" 10 "strings" 11 "time" 12 13 "github.com/restic/restic/internal/backend" 14 "github.com/restic/restic/internal/errors" 15 "github.com/restic/restic/internal/restic" 16 17 "github.com/minio/minio-go" 18 "github.com/minio/minio-go/pkg/credentials" 19 20 "github.com/restic/restic/internal/debug" 21 ) 22 23 // Backend stores data on an S3 endpoint. 24 type Backend struct { 25 client *minio.Client 26 sem *backend.Semaphore 27 cfg Config 28 backend.Layout 29 } 30 31 // make sure that *Backend implements backend.Backend 32 var _ restic.Backend = &Backend{} 33 34 const defaultLayout = "default" 35 36 type chain struct { 37 Providers []credentials.Provider 38 curr credentials.Provider 39 } 40 41 // FIXME: Remove this code once restic migrates to minio-go 4.0.x 42 func newChainCredentials(providers []credentials.Provider) *credentials.Credentials { 43 return credentials.New(&chain{ 44 Providers: append([]credentials.Provider{}, providers...), 45 }) 46 } 47 48 // Retrieve returns the credentials value or error if no provider returned 49 // without error. 50 // 51 // If a provider is found it will be cached and any calls to IsExpired() 52 // will return the expired state of the cached provider. 53 func (c *chain) Retrieve() (credentials.Value, error) { 54 for _, p := range c.Providers { 55 creds, _ := p.Retrieve() 56 // If anonymous proceed to the next provider if any. 57 if creds.SignerType.IsAnonymous() { 58 continue 59 } 60 c.curr = p 61 return creds, nil 62 } 63 return credentials.Value{ 64 SignerType: credentials.SignatureAnonymous, 65 }, nil 66 } 67 68 // IsExpired will returned the expired state of the currently cached provider 69 // if there is one. If there is no current provider, true will be returned. 70 func (c *chain) IsExpired() bool { 71 if c.curr != nil { 72 return c.curr.IsExpired() 73 } 74 75 return true 76 } 77 78 func open(cfg Config, rt http.RoundTripper) (*Backend, error) { 79 debug.Log("open, config %#v", cfg) 80 81 if cfg.MaxRetries > 0 { 82 minio.MaxRetry = int(cfg.MaxRetries) 83 } 84 85 // Chains all credential types, starting with 86 // Static credentials provided by user. 87 // IAM profile based credentials. (performs an HTTP 88 // call to a pre-defined endpoint, only valid inside 89 // configured ec2 instances) 90 // AWS env variables such as AWS_ACCESS_KEY_ID 91 // Minio env variables such as MINIO_ACCESS_KEY 92 creds := newChainCredentials([]credentials.Provider{ 93 &credentials.Static{ 94 Value: credentials.Value{ 95 AccessKeyID: cfg.KeyID, 96 SecretAccessKey: cfg.Secret, 97 }, 98 }, 99 &credentials.IAM{ 100 Client: &http.Client{ 101 Transport: http.DefaultTransport, 102 }, 103 }, 104 &credentials.EnvAWS{}, 105 &credentials.EnvMinio{}, 106 }) 107 client, err := minio.NewWithCredentials(cfg.Endpoint, creds, !cfg.UseHTTP, "") 108 if err != nil { 109 return nil, errors.Wrap(err, "minio.NewWithCredentials") 110 } 111 112 sem, err := backend.NewSemaphore(cfg.Connections) 113 if err != nil { 114 return nil, err 115 } 116 117 be := &Backend{ 118 client: client, 119 sem: sem, 120 cfg: cfg, 121 } 122 123 client.SetCustomTransport(rt) 124 125 l, err := backend.ParseLayout(be, cfg.Layout, defaultLayout, cfg.Prefix) 126 if err != nil { 127 return nil, err 128 } 129 130 be.Layout = l 131 132 return be, nil 133 } 134 135 // Open opens the S3 backend at bucket and region. The bucket is created if it 136 // does not exist yet. 137 func Open(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 138 return open(cfg, rt) 139 } 140 141 // Create opens the S3 backend at bucket and region and creates the bucket if 142 // it does not exist yet. 143 func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 144 be, err := open(cfg, rt) 145 if err != nil { 146 return nil, errors.Wrap(err, "open") 147 } 148 found, err := be.client.BucketExists(cfg.Bucket) 149 if err != nil { 150 debug.Log("BucketExists(%v) returned err %v", cfg.Bucket, err) 151 return nil, errors.Wrap(err, "client.BucketExists") 152 } 153 154 if !found { 155 // create new bucket with default ACL in default region 156 err = be.client.MakeBucket(cfg.Bucket, "") 157 if err != nil { 158 return nil, errors.Wrap(err, "client.MakeBucket") 159 } 160 } 161 162 return be, nil 163 } 164 165 // IsNotExist returns true if the error is caused by a not existing file. 166 func (be *Backend) IsNotExist(err error) bool { 167 debug.Log("IsNotExist(%T, %#v)", err, err) 168 if os.IsNotExist(errors.Cause(err)) { 169 return true 170 } 171 172 if e, ok := errors.Cause(err).(minio.ErrorResponse); ok && e.Code == "NoSuchKey" { 173 return true 174 } 175 176 return false 177 } 178 179 // Join combines path components with slashes. 180 func (be *Backend) Join(p ...string) string { 181 return path.Join(p...) 182 } 183 184 type fileInfo struct { 185 name string 186 size int64 187 mode os.FileMode 188 modTime time.Time 189 isDir bool 190 } 191 192 func (fi fileInfo) Name() string { return fi.name } // base name of the file 193 func (fi fileInfo) Size() int64 { return fi.size } // length in bytes for regular files; system-dependent for others 194 func (fi fileInfo) Mode() os.FileMode { return fi.mode } // file mode bits 195 func (fi fileInfo) ModTime() time.Time { return fi.modTime } // modification time 196 func (fi fileInfo) IsDir() bool { return fi.isDir } // abbreviation for Mode().IsDir() 197 func (fi fileInfo) Sys() interface{} { return nil } // underlying data source (can return nil) 198 199 // ReadDir returns the entries for a directory. 200 func (be *Backend) ReadDir(dir string) (list []os.FileInfo, err error) { 201 debug.Log("ReadDir(%v)", dir) 202 203 // make sure dir ends with a slash 204 if dir[len(dir)-1] != '/' { 205 dir += "/" 206 } 207 208 done := make(chan struct{}) 209 defer close(done) 210 211 for obj := range be.client.ListObjects(be.cfg.Bucket, dir, false, done) { 212 if obj.Key == "" { 213 continue 214 } 215 216 name := strings.TrimPrefix(obj.Key, dir) 217 // Sometimes s3 returns an entry for the dir itself. Ignore it. 218 if name == "" { 219 continue 220 } 221 entry := fileInfo{ 222 name: name, 223 size: obj.Size, 224 modTime: obj.LastModified, 225 } 226 227 if name[len(name)-1] == '/' { 228 entry.isDir = true 229 entry.mode = os.ModeDir | 0755 230 entry.name = name[:len(name)-1] 231 } else { 232 entry.mode = 0644 233 } 234 235 list = append(list, entry) 236 } 237 238 return list, nil 239 } 240 241 // Location returns this backend's location (the bucket name). 242 func (be *Backend) Location() string { 243 return be.Join(be.cfg.Bucket, be.cfg.Prefix) 244 } 245 246 // Path returns the path in the bucket that is used for this backend. 247 func (be *Backend) Path() string { 248 return be.cfg.Prefix 249 } 250 251 // nopCloserFile wraps *os.File and overwrites the Close() method with method 252 // that does nothing. In addition, the method Len() is implemented, which 253 // returns the size of the file (filesize - current offset). 254 type nopCloserFile struct { 255 *os.File 256 } 257 258 func (f nopCloserFile) Close() error { 259 debug.Log("prevented Close()") 260 return nil 261 } 262 263 // Len returns the remaining length of the file (filesize - current offset). 264 func (f nopCloserFile) Len() int { 265 debug.Log("Len() called") 266 fi, err := f.Stat() 267 if err != nil { 268 panic(err) 269 } 270 271 pos, err := f.Seek(0, io.SeekCurrent) 272 if err != nil { 273 panic(err) 274 } 275 276 size := fi.Size() - pos 277 debug.Log("returning file size %v", size) 278 return int(size) 279 } 280 281 type lenner interface { 282 Len() int 283 io.Reader 284 } 285 286 // nopCloserLenner wraps a lenner and overwrites the Close() method with method 287 // that does nothing. In addition, the method Size() is implemented, which 288 // returns the size of the file (filesize - current offset). 289 type nopCloserLenner struct { 290 lenner 291 } 292 293 func (f *nopCloserLenner) Close() error { 294 debug.Log("prevented Close()") 295 return nil 296 } 297 298 // Save stores data in the backend at the handle. 299 func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) { 300 debug.Log("Save %v", h) 301 302 if err := h.Valid(); err != nil { 303 return err 304 } 305 306 objName := be.Filename(h) 307 308 be.sem.GetToken() 309 defer be.sem.ReleaseToken() 310 311 // Check key does not already exist 312 _, err = be.client.StatObject(be.cfg.Bucket, objName) 313 if err == nil { 314 debug.Log("%v already exists", h) 315 return errors.New("key already exists") 316 } 317 318 // FIXME: This is a workaround once we move to minio-go 4.0.x this can be 319 // removed and size can be directly provided. 320 if f, ok := rd.(*os.File); ok { 321 debug.Log("reader is %#T, using nopCloserFile{}", rd) 322 rd = nopCloserFile{f} 323 } else if l, ok := rd.(lenner); ok { 324 debug.Log("reader is %#T, using nopCloserLenner{}", rd) 325 rd = nopCloserLenner{l} 326 } else { 327 debug.Log("reader is %#T, no specific workaround enabled", rd) 328 } 329 330 debug.Log("PutObject(%v, %v)", be.cfg.Bucket, objName) 331 n, err := be.client.PutObject(be.cfg.Bucket, objName, rd, "application/octet-stream") 332 333 debug.Log("%v -> %v bytes, err %#v: %v", objName, n, err, err) 334 335 return errors.Wrap(err, "client.PutObject") 336 } 337 338 // wrapReader wraps an io.ReadCloser to run an additional function on Close. 339 type wrapReader struct { 340 io.ReadCloser 341 f func() 342 } 343 344 func (wr wrapReader) Close() error { 345 err := wr.ReadCloser.Close() 346 wr.f() 347 return err 348 } 349 350 // Load returns a reader that yields the contents of the file at h at the 351 // given offset. If length is nonzero, only a portion of the file is 352 // returned. rd must be closed after use. 353 func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 354 debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h)) 355 if err := h.Valid(); err != nil { 356 return nil, err 357 } 358 359 if offset < 0 { 360 return nil, errors.New("offset is negative") 361 } 362 363 if length < 0 { 364 return nil, errors.Errorf("invalid length %d", length) 365 } 366 367 objName := be.Filename(h) 368 369 byteRange := fmt.Sprintf("bytes=%d-", offset) 370 if length > 0 { 371 byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1) 372 } 373 headers := minio.NewGetReqHeaders() 374 headers.Add("Range", byteRange) 375 376 be.sem.GetToken() 377 debug.Log("Load(%v) send range %v", h, byteRange) 378 379 coreClient := minio.Core{Client: be.client} 380 rd, _, err := coreClient.GetObject(be.cfg.Bucket, objName, headers) 381 if err != nil { 382 be.sem.ReleaseToken() 383 return nil, err 384 } 385 386 closeRd := wrapReader{ 387 ReadCloser: rd, 388 f: func() { 389 debug.Log("Close()") 390 be.sem.ReleaseToken() 391 }, 392 } 393 394 return closeRd, err 395 } 396 397 // Stat returns information about a blob. 398 func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) { 399 debug.Log("%v", h) 400 401 objName := be.Filename(h) 402 var obj *minio.Object 403 404 be.sem.GetToken() 405 obj, err = be.client.GetObject(be.cfg.Bucket, objName) 406 if err != nil { 407 debug.Log("GetObject() err %v", err) 408 be.sem.ReleaseToken() 409 return restic.FileInfo{}, errors.Wrap(err, "client.GetObject") 410 } 411 412 // make sure that the object is closed properly. 413 defer func() { 414 e := obj.Close() 415 be.sem.ReleaseToken() 416 if err == nil { 417 err = errors.Wrap(e, "Close") 418 } 419 }() 420 421 fi, err := obj.Stat() 422 if err != nil { 423 debug.Log("Stat() err %v", err) 424 return restic.FileInfo{}, errors.Wrap(err, "Stat") 425 } 426 427 return restic.FileInfo{Size: fi.Size}, nil 428 } 429 430 // Test returns true if a blob of the given type and name exists in the backend. 431 func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) { 432 found := false 433 objName := be.Filename(h) 434 435 be.sem.GetToken() 436 _, err := be.client.StatObject(be.cfg.Bucket, objName) 437 be.sem.ReleaseToken() 438 439 if err == nil { 440 found = true 441 } 442 443 // If error, then not found 444 return found, nil 445 } 446 447 // Remove removes the blob with the given name and type. 448 func (be *Backend) Remove(ctx context.Context, h restic.Handle) error { 449 objName := be.Filename(h) 450 451 be.sem.GetToken() 452 err := be.client.RemoveObject(be.cfg.Bucket, objName) 453 be.sem.ReleaseToken() 454 455 debug.Log("Remove(%v) at %v -> err %v", h, objName, err) 456 457 if be.IsNotExist(err) { 458 err = nil 459 } 460 461 return errors.Wrap(err, "client.RemoveObject") 462 } 463 464 // List returns a channel that yields all names of blobs of type t. A 465 // goroutine is started for this. If the channel done is closed, sending 466 // stops. 467 func (be *Backend) List(ctx context.Context, t restic.FileType) <-chan string { 468 debug.Log("listing %v", t) 469 ch := make(chan string) 470 471 prefix := be.Dirname(restic.Handle{Type: t}) 472 473 // make sure prefix ends with a slash 474 if prefix[len(prefix)-1] != '/' { 475 prefix += "/" 476 } 477 478 // NB: unfortunately we can't protect this with be.sem.GetToken() here. 479 // Doing so would enable a deadlock situation (gh-1399), as ListObjects() 480 // starts its own goroutine and returns results via a channel. 481 listresp := be.client.ListObjects(be.cfg.Bucket, prefix, true, ctx.Done()) 482 483 go func() { 484 defer close(ch) 485 for obj := range listresp { 486 m := strings.TrimPrefix(obj.Key, prefix) 487 if m == "" { 488 continue 489 } 490 491 select { 492 case ch <- path.Base(m): 493 case <-ctx.Done(): 494 return 495 } 496 } 497 }() 498 499 return ch 500 } 501 502 // Remove keys for a specified backend type. 503 func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error { 504 for key := range be.List(ctx, restic.DataFile) { 505 err := be.Remove(ctx, restic.Handle{Type: restic.DataFile, Name: key}) 506 if err != nil { 507 return err 508 } 509 } 510 511 return nil 512 } 513 514 // Delete removes all restic keys in the bucket. It will not remove the bucket itself. 515 func (be *Backend) Delete(ctx context.Context) error { 516 alltypes := []restic.FileType{ 517 restic.DataFile, 518 restic.KeyFile, 519 restic.LockFile, 520 restic.SnapshotFile, 521 restic.IndexFile} 522 523 for _, t := range alltypes { 524 err := be.removeKeys(ctx, t) 525 if err != nil { 526 return nil 527 } 528 } 529 530 return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 531 } 532 533 // Close does nothing 534 func (be *Backend) Close() error { return nil } 535 536 // Rename moves a file based on the new layout l. 537 func (be *Backend) Rename(h restic.Handle, l backend.Layout) error { 538 debug.Log("Rename %v to %v", h, l) 539 oldname := be.Filename(h) 540 newname := l.Filename(h) 541 542 if oldname == newname { 543 debug.Log(" %v is already renamed", newname) 544 return nil 545 } 546 547 debug.Log(" %v -> %v", oldname, newname) 548 549 src := minio.NewSourceInfo(be.cfg.Bucket, oldname, nil) 550 551 dst, err := minio.NewDestinationInfo(be.cfg.Bucket, newname, nil, nil) 552 if err != nil { 553 return errors.Wrap(err, "NewDestinationInfo") 554 } 555 556 err = be.client.CopyObject(dst, src) 557 if err != nil && be.IsNotExist(err) { 558 debug.Log("copy failed: %v, seems to already have been renamed", err) 559 return nil 560 } 561 562 if err != nil { 563 debug.Log("copy failed: %v", err) 564 return err 565 } 566 567 return be.client.RemoveObject(be.cfg.Bucket, oldname) 568 }