github.com/mckael/restic@v0.8.3/internal/backend/gs/gs.go (about) 1 // Package gs provides a restic backend for Google Cloud Storage. 2 package gs 3 4 import ( 5 "context" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "path" 11 "strings" 12 13 "github.com/pkg/errors" 14 "github.com/restic/restic/internal/backend" 15 "github.com/restic/restic/internal/debug" 16 "github.com/restic/restic/internal/restic" 17 18 "io/ioutil" 19 20 "golang.org/x/oauth2" 21 "golang.org/x/oauth2/google" 22 "google.golang.org/api/googleapi" 23 storage "google.golang.org/api/storage/v1" 24 ) 25 26 // Backend stores data in a GCS bucket. 27 // 28 // The service account used to access the bucket must have these permissions: 29 // * storage.objects.create 30 // * storage.objects.delete 31 // * storage.objects.get 32 // * storage.objects.list 33 type Backend struct { 34 service *storage.Service 35 projectID string 36 sem *backend.Semaphore 37 bucketName string 38 prefix string 39 listMaxItems int 40 backend.Layout 41 } 42 43 // Ensure that *Backend implements restic.Backend. 44 var _ restic.Backend = &Backend{} 45 46 func getStorageService(jsonKeyPath string, rt http.RoundTripper) (*storage.Service, error) { 47 48 raw, err := ioutil.ReadFile(jsonKeyPath) 49 if err != nil { 50 return nil, errors.Wrap(err, "ReadFile") 51 } 52 53 conf, err := google.JWTConfigFromJSON(raw, storage.DevstorageReadWriteScope) 54 if err != nil { 55 return nil, err 56 } 57 58 // create a new HTTP client 59 httpClient := &http.Client{ 60 Transport: rt, 61 } 62 63 // create a now context with the HTTP client stored at the oauth2.HTTPClient key 64 ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) 65 66 // then pass this context to Client(), which returns a new HTTP client 67 client := conf.Client(ctx) 68 69 // that we can then pass to New() 70 service, err := storage.New(client) 71 if err != nil { 72 return nil, err 73 } 74 75 return service, nil 76 } 77 78 const defaultListMaxItems = 1000 79 80 func open(cfg Config, rt http.RoundTripper) (*Backend, error) { 81 debug.Log("open, config %#v", cfg) 82 83 service, err := getStorageService(cfg.JSONKeyPath, rt) 84 if err != nil { 85 return nil, errors.Wrap(err, "getStorageService") 86 } 87 88 sem, err := backend.NewSemaphore(cfg.Connections) 89 if err != nil { 90 return nil, err 91 } 92 93 be := &Backend{ 94 service: service, 95 projectID: cfg.ProjectID, 96 sem: sem, 97 bucketName: cfg.Bucket, 98 prefix: cfg.Prefix, 99 Layout: &backend.DefaultLayout{ 100 Path: cfg.Prefix, 101 Join: path.Join, 102 }, 103 listMaxItems: defaultListMaxItems, 104 } 105 106 return be, nil 107 } 108 109 // Open opens the gs backend at the specified bucket. 110 func Open(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 111 return open(cfg, rt) 112 } 113 114 // Create opens the gs backend at the specified bucket and attempts to creates 115 // the bucket if it does not exist yet. 116 // 117 // The service account must have the "storage.buckets.create" permission to 118 // create a bucket the does not yet exist. 119 func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 120 be, err := open(cfg, rt) 121 if err != nil { 122 return nil, errors.Wrap(err, "open") 123 } 124 125 // Try to determine if the bucket exists. If it does not, try to create it. 126 // 127 // A Get call has three typical error cases: 128 // 129 // * nil: Bucket exists and we have access to the metadata (returned). 130 // 131 // * 403: Bucket exists and we do not have access to the metadata. We 132 // don't have storage.buckets.get permission to the bucket, but we may 133 // still be able to access objects in the bucket. 134 // 135 // * 404: Bucket doesn't exist. 136 // 137 // Determining if the bucket is accessible is best-effort because the 138 // 403 case is ambiguous. 139 if _, err := be.service.Buckets.Get(be.bucketName).Do(); err != nil { 140 gerr, ok := err.(*googleapi.Error) 141 if !ok { 142 // Don't know what to do with this error. 143 return nil, errors.Wrap(err, "service.Buckets.Get") 144 } 145 146 switch gerr.Code { 147 case 403: 148 // Bucket exists, but we don't know if it is 149 // accessible. Optimistically assume it is; if not, 150 // future Backend calls will fail. 151 debug.Log("Unable to determine if bucket %s is accessible (err %v). Continuing as if it is.", be.bucketName, err) 152 case 404: 153 // Bucket doesn't exist, try to create it. 154 bucket := &storage.Bucket{ 155 Name: be.bucketName, 156 } 157 158 if _, err := be.service.Buckets.Insert(be.projectID, bucket).Do(); err != nil { 159 // Always an error, as the bucket definitely 160 // doesn't exist. 161 return nil, errors.Wrap(err, "service.Buckets.Insert") 162 } 163 default: 164 // Don't know what to do with this error. 165 return nil, errors.Wrap(err, "service.Buckets.Get") 166 } 167 } 168 169 return be, nil 170 } 171 172 // SetListMaxItems sets the number of list items to load per request. 173 func (be *Backend) SetListMaxItems(i int) { 174 be.listMaxItems = i 175 } 176 177 // IsNotExist returns true if the error is caused by a not existing file. 178 func (be *Backend) IsNotExist(err error) bool { 179 debug.Log("IsNotExist(%T, %#v)", err, err) 180 181 if os.IsNotExist(err) { 182 return true 183 } 184 185 if er, ok := err.(*googleapi.Error); ok { 186 if er.Code == 404 { 187 return true 188 } 189 } 190 191 return false 192 } 193 194 // Join combines path components with slashes. 195 func (be *Backend) Join(p ...string) string { 196 return path.Join(p...) 197 } 198 199 // Location returns this backend's location (the bucket name). 200 func (be *Backend) Location() string { 201 return be.Join(be.bucketName, be.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.prefix 207 } 208 209 // Save stores data in the backend at the handle. 210 func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) { 211 if err := h.Valid(); err != nil { 212 return err 213 } 214 215 objName := be.Filename(h) 216 217 debug.Log("Save %v at %v", h, objName) 218 219 be.sem.GetToken() 220 221 debug.Log("InsertObject(%v, %v)", be.bucketName, objName) 222 223 // Set chunk size to zero to disable resumable uploads. 224 // 225 // With a non-zero chunk size (the default is 226 // googleapi.DefaultUploadChunkSize, 8MB), Insert will buffer data from 227 // rd in chunks of this size so it can upload these chunks in 228 // individual requests. 229 // 230 // This chunking allows the library to automatically handle network 231 // interruptions and re-upload only the last chunk rather than the full 232 // file. 233 // 234 // Unfortunately, this buffering doesn't play nicely with 235 // --limit-upload, which applies a rate limit to rd. This rate limit 236 // ends up only limiting the read from rd into the buffer rather than 237 // the network traffic itself. This results in poor network rate limit 238 // behavior, where individual chunks are written to the network at full 239 // bandwidth for several seconds, followed by several seconds of no 240 // network traffic as the next chunk is read through the rate limiter. 241 // 242 // By disabling chunking, rd is passed further down the request stack, 243 // where there is less (but some) buffering, which ultimately results 244 // in better rate limiting behavior. 245 // 246 // restic typically writes small blobs (4MB-30MB), so the resumable 247 // uploads are not providing significant benefit anyways. 248 cs := googleapi.ChunkSize(0) 249 250 info, err := be.service.Objects.Insert(be.bucketName, 251 &storage.Object{ 252 Name: objName, 253 }).Media(rd, cs).Do() 254 255 be.sem.ReleaseToken() 256 257 if err != nil { 258 debug.Log("%v: err %#v: %v", objName, err, err) 259 return errors.Wrap(err, "service.Objects.Insert") 260 } 261 262 debug.Log("%v -> %v bytes", objName, info.Size) 263 return nil 264 } 265 266 // wrapReader wraps an io.ReadCloser to run an additional function on Close. 267 type wrapReader struct { 268 io.ReadCloser 269 f func() 270 } 271 272 func (wr wrapReader) Close() error { 273 err := wr.ReadCloser.Close() 274 wr.f() 275 return err 276 } 277 278 // Load runs fn with a reader that yields the contents of the file at h at the 279 // given offset. 280 func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error { 281 return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn) 282 } 283 284 func (be *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 285 debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h)) 286 if err := h.Valid(); err != nil { 287 return nil, err 288 } 289 290 if offset < 0 { 291 return nil, errors.New("offset is negative") 292 } 293 294 if length < 0 { 295 return nil, errors.Errorf("invalid length %d", length) 296 } 297 298 objName := be.Filename(h) 299 300 be.sem.GetToken() 301 302 var byteRange string 303 if length > 0 { 304 byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length-1)) 305 } else { 306 byteRange = fmt.Sprintf("bytes=%d-", offset) 307 } 308 309 req := be.service.Objects.Get(be.bucketName, objName) 310 // https://cloud.google.com/storage/docs/json_api/v1/parameters#range 311 req.Header().Set("Range", byteRange) 312 res, err := req.Download() 313 if err != nil { 314 be.sem.ReleaseToken() 315 return nil, err 316 } 317 318 closeRd := wrapReader{ 319 ReadCloser: res.Body, 320 f: func() { 321 debug.Log("Close()") 322 be.sem.ReleaseToken() 323 }, 324 } 325 326 return closeRd, err 327 } 328 329 // Stat returns information about a blob. 330 func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) { 331 debug.Log("%v", h) 332 333 objName := be.Filename(h) 334 335 be.sem.GetToken() 336 obj, err := be.service.Objects.Get(be.bucketName, objName).Do() 337 be.sem.ReleaseToken() 338 339 if err != nil { 340 debug.Log("GetObject() err %v", err) 341 return restic.FileInfo{}, errors.Wrap(err, "service.Objects.Get") 342 } 343 344 return restic.FileInfo{Size: int64(obj.Size), Name: h.Name}, nil 345 } 346 347 // Test returns true if a blob of the given type and name exists in the backend. 348 func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) { 349 found := false 350 objName := be.Filename(h) 351 352 be.sem.GetToken() 353 _, err := be.service.Objects.Get(be.bucketName, objName).Do() 354 be.sem.ReleaseToken() 355 356 if err == nil { 357 found = true 358 } 359 // If error, then not found 360 return found, nil 361 } 362 363 // Remove removes the blob with the given name and type. 364 func (be *Backend) Remove(ctx context.Context, h restic.Handle) error { 365 objName := be.Filename(h) 366 367 be.sem.GetToken() 368 err := be.service.Objects.Delete(be.bucketName, objName).Do() 369 be.sem.ReleaseToken() 370 371 if er, ok := err.(*googleapi.Error); ok { 372 if er.Code == 404 { 373 err = nil 374 } 375 } 376 377 debug.Log("Remove(%v) at %v -> err %v", h, objName, err) 378 return errors.Wrap(err, "client.RemoveObject") 379 } 380 381 // List runs fn for each file in the backend which has the type t. When an 382 // error occurs (or fn returns an error), List stops and returns it. 383 func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error { 384 debug.Log("listing %v", t) 385 386 prefix, _ := be.Basedir(t) 387 388 // make sure prefix ends with a slash 389 if !strings.HasSuffix(prefix, "/") { 390 prefix += "/" 391 } 392 393 ctx, cancel := context.WithCancel(ctx) 394 defer cancel() 395 396 listReq := be.service.Objects.List(be.bucketName).Context(ctx).Prefix(prefix).MaxResults(int64(be.listMaxItems)) 397 for { 398 be.sem.GetToken() 399 obj, err := listReq.Do() 400 be.sem.ReleaseToken() 401 402 if err != nil { 403 return err 404 } 405 406 debug.Log("returned %v items", len(obj.Items)) 407 408 for _, item := range obj.Items { 409 m := strings.TrimPrefix(item.Name, prefix) 410 if m == "" { 411 continue 412 } 413 414 if ctx.Err() != nil { 415 return ctx.Err() 416 } 417 418 fi := restic.FileInfo{ 419 Name: path.Base(m), 420 Size: int64(item.Size), 421 } 422 423 err := fn(fi) 424 if err != nil { 425 return err 426 } 427 428 if ctx.Err() != nil { 429 return ctx.Err() 430 } 431 } 432 433 if obj.NextPageToken == "" { 434 break 435 } 436 listReq.PageToken(obj.NextPageToken) 437 } 438 439 return ctx.Err() 440 } 441 442 // Remove keys for a specified backend type. 443 func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error { 444 return be.List(ctx, t, func(fi restic.FileInfo) error { 445 return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name}) 446 }) 447 } 448 449 // Delete removes all restic keys in the bucket. It will not remove the bucket itself. 450 func (be *Backend) Delete(ctx context.Context) error { 451 alltypes := []restic.FileType{ 452 restic.DataFile, 453 restic.KeyFile, 454 restic.LockFile, 455 restic.SnapshotFile, 456 restic.IndexFile} 457 458 for _, t := range alltypes { 459 err := be.removeKeys(ctx, t) 460 if err != nil { 461 return nil 462 } 463 } 464 465 return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 466 } 467 468 // Close does nothing. 469 func (be *Backend) Close() error { return nil }