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