github.com/mckael/restic@v0.8.3/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 var size int64 = -1 239 240 type lenner interface { 241 Len() int 242 } 243 244 // find size for reader 245 if f, ok := rd.(*os.File); ok { 246 size, err = lenForFile(f) 247 if err != nil { 248 return err 249 } 250 } else if l, ok := rd.(lenner); ok { 251 size = int64(l.Len()) 252 } 253 254 opts := minio.PutObjectOptions{} 255 opts.ContentType = "application/octet-stream" 256 257 debug.Log("PutObject(%v, %v, %v)", be.cfg.Bucket, objName, size) 258 n, err := be.client.PutObjectWithContext(ctx, be.cfg.Bucket, objName, ioutil.NopCloser(rd), size, opts) 259 260 debug.Log("%v -> %v bytes, err %#v: %v", objName, n, err, err) 261 262 return errors.Wrap(err, "client.PutObject") 263 } 264 265 // wrapReader wraps an io.ReadCloser to run an additional function on Close. 266 type wrapReader struct { 267 io.ReadCloser 268 f func() 269 } 270 271 func (wr wrapReader) Close() error { 272 err := wr.ReadCloser.Close() 273 wr.f() 274 return err 275 } 276 277 // Load runs fn with a reader that yields the contents of the file at h at the 278 // given offset. 279 func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error { 280 return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn) 281 } 282 283 func (be *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 284 debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h)) 285 if err := h.Valid(); err != nil { 286 return nil, err 287 } 288 289 if offset < 0 { 290 return nil, errors.New("offset is negative") 291 } 292 293 if length < 0 { 294 return nil, errors.Errorf("invalid length %d", length) 295 } 296 297 objName := be.Filename(h) 298 opts := minio.GetObjectOptions{} 299 300 var err error 301 if length > 0 { 302 debug.Log("range: %v-%v", offset, offset+int64(length)-1) 303 err = opts.SetRange(offset, offset+int64(length)-1) 304 } else if offset > 0 { 305 debug.Log("range: %v-", offset) 306 err = opts.SetRange(offset, 0) 307 } 308 309 if err != nil { 310 return nil, errors.Wrap(err, "SetRange") 311 } 312 313 be.sem.GetToken() 314 coreClient := minio.Core{Client: be.client} 315 rd, err := coreClient.GetObjectWithContext(ctx, be.cfg.Bucket, objName, opts) 316 if err != nil { 317 be.sem.ReleaseToken() 318 return nil, err 319 } 320 321 closeRd := wrapReader{ 322 ReadCloser: rd, 323 f: func() { 324 debug.Log("Close()") 325 be.sem.ReleaseToken() 326 }, 327 } 328 329 return closeRd, err 330 } 331 332 // Stat returns information about a blob. 333 func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) { 334 debug.Log("%v", h) 335 336 objName := be.Filename(h) 337 var obj *minio.Object 338 339 opts := minio.GetObjectOptions{} 340 341 be.sem.GetToken() 342 obj, err = be.client.GetObjectWithContext(ctx, be.cfg.Bucket, objName, opts) 343 if err != nil { 344 debug.Log("GetObject() err %v", err) 345 be.sem.ReleaseToken() 346 return restic.FileInfo{}, errors.Wrap(err, "client.GetObject") 347 } 348 349 // make sure that the object is closed properly. 350 defer func() { 351 e := obj.Close() 352 be.sem.ReleaseToken() 353 if err == nil { 354 err = errors.Wrap(e, "Close") 355 } 356 }() 357 358 fi, err := obj.Stat() 359 if err != nil { 360 debug.Log("Stat() err %v", err) 361 return restic.FileInfo{}, errors.Wrap(err, "Stat") 362 } 363 364 return restic.FileInfo{Size: fi.Size, Name: h.Name}, nil 365 } 366 367 // Test returns true if a blob of the given type and name exists in the backend. 368 func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) { 369 found := false 370 objName := be.Filename(h) 371 372 be.sem.GetToken() 373 _, err := be.client.StatObject(be.cfg.Bucket, objName, minio.StatObjectOptions{}) 374 be.sem.ReleaseToken() 375 376 if err == nil { 377 found = true 378 } 379 380 // If error, then not found 381 return found, nil 382 } 383 384 // Remove removes the blob with the given name and type. 385 func (be *Backend) Remove(ctx context.Context, h restic.Handle) error { 386 objName := be.Filename(h) 387 388 be.sem.GetToken() 389 err := be.client.RemoveObject(be.cfg.Bucket, objName) 390 be.sem.ReleaseToken() 391 392 debug.Log("Remove(%v) at %v -> err %v", h, objName, err) 393 394 if be.IsNotExist(err) { 395 err = nil 396 } 397 398 return errors.Wrap(err, "client.RemoveObject") 399 } 400 401 // List runs fn for each file in the backend which has the type t. When an 402 // error occurs (or fn returns an error), List stops and returns it. 403 func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error { 404 debug.Log("listing %v", t) 405 406 prefix, recursive := be.Basedir(t) 407 408 // make sure prefix ends with a slash 409 if !strings.HasSuffix(prefix, "/") { 410 prefix += "/" 411 } 412 413 ctx, cancel := context.WithCancel(ctx) 414 defer cancel() 415 416 // NB: unfortunately we can't protect this with be.sem.GetToken() here. 417 // Doing so would enable a deadlock situation (gh-1399), as ListObjects() 418 // starts its own goroutine and returns results via a channel. 419 listresp := be.client.ListObjects(be.cfg.Bucket, prefix, recursive, ctx.Done()) 420 421 for obj := range listresp { 422 m := strings.TrimPrefix(obj.Key, prefix) 423 if m == "" { 424 continue 425 } 426 427 fi := restic.FileInfo{ 428 Name: path.Base(m), 429 Size: obj.Size, 430 } 431 432 if ctx.Err() != nil { 433 return ctx.Err() 434 } 435 436 err := fn(fi) 437 if err != nil { 438 return err 439 } 440 441 if ctx.Err() != nil { 442 return ctx.Err() 443 } 444 } 445 446 return ctx.Err() 447 } 448 449 // Remove keys for a specified backend type. 450 func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error { 451 return be.List(ctx, restic.DataFile, func(fi restic.FileInfo) error { 452 return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name}) 453 }) 454 } 455 456 // Delete removes all restic keys in the bucket. It will not remove the bucket itself. 457 func (be *Backend) Delete(ctx context.Context) error { 458 alltypes := []restic.FileType{ 459 restic.DataFile, 460 restic.KeyFile, 461 restic.LockFile, 462 restic.SnapshotFile, 463 restic.IndexFile} 464 465 for _, t := range alltypes { 466 err := be.removeKeys(ctx, t) 467 if err != nil { 468 return nil 469 } 470 } 471 472 return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 473 } 474 475 // Close does nothing 476 func (be *Backend) Close() error { return nil } 477 478 // Rename moves a file based on the new layout l. 479 func (be *Backend) Rename(h restic.Handle, l backend.Layout) error { 480 debug.Log("Rename %v to %v", h, l) 481 oldname := be.Filename(h) 482 newname := l.Filename(h) 483 484 if oldname == newname { 485 debug.Log(" %v is already renamed", newname) 486 return nil 487 } 488 489 debug.Log(" %v -> %v", oldname, newname) 490 491 src := minio.NewSourceInfo(be.cfg.Bucket, oldname, nil) 492 493 dst, err := minio.NewDestinationInfo(be.cfg.Bucket, newname, nil, nil) 494 if err != nil { 495 return errors.Wrap(err, "NewDestinationInfo") 496 } 497 498 err = be.client.CopyObject(dst, src) 499 if err != nil && be.IsNotExist(err) { 500 debug.Log("copy failed: %v, seems to already have been renamed", err) 501 return nil 502 } 503 504 if err != nil { 505 debug.Log("copy failed: %v", err) 506 return err 507 } 508 509 return be.client.RemoveObject(be.cfg.Bucket, oldname) 510 }