github.com/mckael/restic@v0.8.3/internal/backend/b2/b2.go (about) 1 package b2 2 3 import ( 4 "context" 5 "io" 6 "net/http" 7 "path" 8 "strings" 9 10 "github.com/restic/restic/internal/backend" 11 "github.com/restic/restic/internal/debug" 12 "github.com/restic/restic/internal/errors" 13 "github.com/restic/restic/internal/restic" 14 15 "github.com/kurin/blazer/b2" 16 ) 17 18 // b2Backend is a backend which stores its data on Backblaze B2. 19 type b2Backend struct { 20 client *b2.Client 21 bucket *b2.Bucket 22 cfg Config 23 listMaxItems int 24 backend.Layout 25 sem *backend.Semaphore 26 } 27 28 const defaultListMaxItems = 1000 29 30 // ensure statically that *b2Backend implements restic.Backend. 31 var _ restic.Backend = &b2Backend{} 32 33 func newClient(ctx context.Context, cfg Config, rt http.RoundTripper) (*b2.Client, error) { 34 opts := []b2.ClientOption{b2.Transport(rt)} 35 36 c, err := b2.NewClient(ctx, cfg.AccountID, cfg.Key, opts...) 37 if err != nil { 38 return nil, errors.Wrap(err, "b2.NewClient") 39 } 40 return c, nil 41 } 42 43 // Open opens a connection to the B2 service. 44 func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) { 45 debug.Log("cfg %#v", cfg) 46 47 ctx, cancel := context.WithCancel(ctx) 48 defer cancel() 49 50 client, err := newClient(ctx, cfg, rt) 51 if err != nil { 52 return nil, err 53 } 54 55 bucket, err := client.Bucket(ctx, cfg.Bucket) 56 if err != nil { 57 return nil, errors.Wrap(err, "Bucket") 58 } 59 60 sem, err := backend.NewSemaphore(cfg.Connections) 61 if err != nil { 62 return nil, err 63 } 64 65 be := &b2Backend{ 66 client: client, 67 bucket: bucket, 68 cfg: cfg, 69 Layout: &backend.DefaultLayout{ 70 Join: path.Join, 71 Path: cfg.Prefix, 72 }, 73 listMaxItems: defaultListMaxItems, 74 sem: sem, 75 } 76 77 return be, nil 78 } 79 80 // Create opens a connection to the B2 service. If the bucket does not exist yet, 81 // it is created. 82 func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) { 83 debug.Log("cfg %#v", cfg) 84 85 ctx, cancel := context.WithCancel(ctx) 86 defer cancel() 87 88 client, err := newClient(ctx, cfg, rt) 89 if err != nil { 90 return nil, err 91 } 92 93 attr := b2.BucketAttrs{ 94 Type: b2.Private, 95 } 96 bucket, err := client.NewBucket(ctx, cfg.Bucket, &attr) 97 if err != nil { 98 return nil, errors.Wrap(err, "NewBucket") 99 } 100 101 sem, err := backend.NewSemaphore(cfg.Connections) 102 if err != nil { 103 return nil, err 104 } 105 106 be := &b2Backend{ 107 client: client, 108 bucket: bucket, 109 cfg: cfg, 110 Layout: &backend.DefaultLayout{ 111 Join: path.Join, 112 Path: cfg.Prefix, 113 }, 114 listMaxItems: defaultListMaxItems, 115 sem: sem, 116 } 117 118 present, err := be.Test(ctx, restic.Handle{Type: restic.ConfigFile}) 119 if err != nil { 120 return nil, err 121 } 122 123 if present { 124 return nil, errors.New("config already exists") 125 } 126 127 return be, nil 128 } 129 130 // SetListMaxItems sets the number of list items to load per request. 131 func (be *b2Backend) SetListMaxItems(i int) { 132 be.listMaxItems = i 133 } 134 135 // Location returns the location for the backend. 136 func (be *b2Backend) Location() string { 137 return be.cfg.Bucket 138 } 139 140 // IsNotExist returns true if the error is caused by a non-existing file. 141 func (be *b2Backend) IsNotExist(err error) bool { 142 return b2.IsNotExist(errors.Cause(err)) 143 } 144 145 // Load runs fn with a reader that yields the contents of the file at h at the 146 // given offset. 147 func (be *b2Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error { 148 return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn) 149 } 150 151 func (be *b2Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 152 debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h)) 153 if err := h.Valid(); err != nil { 154 return nil, err 155 } 156 157 if offset < 0 { 158 return nil, errors.New("offset is negative") 159 } 160 161 if length < 0 { 162 return nil, errors.Errorf("invalid length %d", length) 163 } 164 165 ctx, cancel := context.WithCancel(ctx) 166 167 be.sem.GetToken() 168 169 name := be.Layout.Filename(h) 170 obj := be.bucket.Object(name) 171 172 if offset == 0 && length == 0 { 173 rd := obj.NewReader(ctx) 174 return be.sem.ReleaseTokenOnClose(rd, cancel), nil 175 } 176 177 // pass a negative length to NewRangeReader so that the remainder of the 178 // file is read. 179 if length == 0 { 180 length = -1 181 } 182 183 rd := obj.NewRangeReader(ctx, offset, int64(length)) 184 return be.sem.ReleaseTokenOnClose(rd, cancel), nil 185 } 186 187 // Save stores data in the backend at the handle. 188 func (be *b2Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) error { 189 ctx, cancel := context.WithCancel(ctx) 190 defer cancel() 191 192 if err := h.Valid(); err != nil { 193 return err 194 } 195 196 be.sem.GetToken() 197 defer be.sem.ReleaseToken() 198 199 name := be.Filename(h) 200 debug.Log("Save %v, name %v", h, name) 201 obj := be.bucket.Object(name) 202 203 w := obj.NewWriter(ctx) 204 n, err := io.Copy(w, rd) 205 debug.Log(" saved %d bytes, err %v", n, err) 206 207 if err != nil { 208 _ = w.Close() 209 return errors.Wrap(err, "Copy") 210 } 211 212 return errors.Wrap(w.Close(), "Close") 213 } 214 215 // Stat returns information about a blob. 216 func (be *b2Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) { 217 debug.Log("Stat %v", h) 218 219 be.sem.GetToken() 220 defer be.sem.ReleaseToken() 221 222 name := be.Filename(h) 223 obj := be.bucket.Object(name) 224 info, err := obj.Attrs(ctx) 225 if err != nil { 226 debug.Log("Attrs() err %v", err) 227 return restic.FileInfo{}, errors.Wrap(err, "Stat") 228 } 229 return restic.FileInfo{Size: info.Size, Name: h.Name}, nil 230 } 231 232 // Test returns true if a blob of the given type and name exists in the backend. 233 func (be *b2Backend) Test(ctx context.Context, h restic.Handle) (bool, error) { 234 debug.Log("Test %v", h) 235 236 be.sem.GetToken() 237 defer be.sem.ReleaseToken() 238 239 found := false 240 name := be.Filename(h) 241 obj := be.bucket.Object(name) 242 info, err := obj.Attrs(ctx) 243 if err == nil && info != nil && info.Status == b2.Uploaded { 244 found = true 245 } 246 return found, nil 247 } 248 249 // Remove removes the blob with the given name and type. 250 func (be *b2Backend) Remove(ctx context.Context, h restic.Handle) error { 251 debug.Log("Remove %v", h) 252 253 be.sem.GetToken() 254 defer be.sem.ReleaseToken() 255 256 obj := be.bucket.Object(be.Filename(h)) 257 return errors.Wrap(obj.Delete(ctx), "Delete") 258 } 259 260 // List returns a channel that yields all names of blobs of type t. A 261 // goroutine is started for this. If the channel done is closed, sending 262 // stops. 263 func (be *b2Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error { 264 debug.Log("List %v", t) 265 266 prefix, _ := be.Basedir(t) 267 cur := &b2.Cursor{Prefix: prefix} 268 269 ctx, cancel := context.WithCancel(ctx) 270 defer cancel() 271 272 for { 273 be.sem.GetToken() 274 objs, c, err := be.bucket.ListCurrentObjects(ctx, be.listMaxItems, cur) 275 be.sem.ReleaseToken() 276 277 if err != nil && err != io.EOF { 278 debug.Log("List: %v", err) 279 return err 280 } 281 282 debug.Log("returned %v items", len(objs)) 283 for _, obj := range objs { 284 // Skip objects returned that do not have the specified prefix. 285 if !strings.HasPrefix(obj.Name(), prefix) { 286 continue 287 } 288 289 m := path.Base(obj.Name()) 290 if m == "" { 291 continue 292 } 293 294 if ctx.Err() != nil { 295 return ctx.Err() 296 } 297 298 if ctx.Err() != nil { 299 return ctx.Err() 300 } 301 302 attrs, err := obj.Attrs(ctx) 303 if err != nil { 304 return err 305 } 306 307 fi := restic.FileInfo{ 308 Name: m, 309 Size: attrs.Size, 310 } 311 312 err = fn(fi) 313 if err != nil { 314 return err 315 } 316 317 if ctx.Err() != nil { 318 return ctx.Err() 319 } 320 } 321 322 if err == io.EOF { 323 return ctx.Err() 324 } 325 cur = c 326 } 327 328 return ctx.Err() 329 } 330 331 // Remove keys for a specified backend type. 332 func (be *b2Backend) removeKeys(ctx context.Context, t restic.FileType) error { 333 debug.Log("removeKeys %v", t) 334 return be.List(ctx, t, func(fi restic.FileInfo) error { 335 return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name}) 336 }) 337 } 338 339 // Delete removes all restic keys in the bucket. It will not remove the bucket itself. 340 func (be *b2Backend) Delete(ctx context.Context) error { 341 alltypes := []restic.FileType{ 342 restic.DataFile, 343 restic.KeyFile, 344 restic.LockFile, 345 restic.SnapshotFile, 346 restic.IndexFile} 347 348 for _, t := range alltypes { 349 err := be.removeKeys(ctx, t) 350 if err != nil { 351 return nil 352 } 353 } 354 err := be.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 355 if err != nil && b2.IsNotExist(errors.Cause(err)) { 356 err = nil 357 } 358 359 return err 360 } 361 362 // Close does nothing 363 func (be *b2Backend) Close() error { return nil }