github.com/mckael/restic@v0.8.3/internal/backend/azure/azure.go (about) 1 package azure 2 3 import ( 4 "context" 5 "io" 6 "net/http" 7 "os" 8 "path" 9 "strings" 10 11 "github.com/Azure/azure-sdk-for-go/storage" 12 "github.com/restic/restic/internal/backend" 13 "github.com/restic/restic/internal/debug" 14 "github.com/restic/restic/internal/errors" 15 "github.com/restic/restic/internal/restic" 16 ) 17 18 // Backend stores data on an azure endpoint. 19 type Backend struct { 20 accountName string 21 container *storage.Container 22 sem *backend.Semaphore 23 prefix string 24 listMaxItems int 25 backend.Layout 26 } 27 28 const defaultListMaxItems = 5000 29 30 // make sure that *Backend implements backend.Backend 31 var _ restic.Backend = &Backend{} 32 33 func open(cfg Config, rt http.RoundTripper) (*Backend, error) { 34 debug.Log("open, config %#v", cfg) 35 36 client, err := storage.NewBasicClient(cfg.AccountName, cfg.AccountKey) 37 if err != nil { 38 return nil, errors.Wrap(err, "NewBasicClient") 39 } 40 41 client.HTTPClient = &http.Client{Transport: rt} 42 43 service := client.GetBlobService() 44 45 sem, err := backend.NewSemaphore(cfg.Connections) 46 if err != nil { 47 return nil, err 48 } 49 50 be := &Backend{ 51 container: service.GetContainerReference(cfg.Container), 52 accountName: cfg.AccountName, 53 sem: sem, 54 prefix: cfg.Prefix, 55 Layout: &backend.DefaultLayout{ 56 Path: cfg.Prefix, 57 Join: path.Join, 58 }, 59 listMaxItems: defaultListMaxItems, 60 } 61 62 return be, nil 63 } 64 65 // Open opens the Azure backend at specified container. 66 func Open(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 67 return open(cfg, rt) 68 } 69 70 // Create opens the Azure backend at specified container and creates the container if 71 // it does not exist yet. 72 func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) { 73 be, err := open(cfg, rt) 74 75 if err != nil { 76 return nil, errors.Wrap(err, "open") 77 } 78 79 options := storage.CreateContainerOptions{ 80 Access: storage.ContainerAccessTypePrivate, 81 } 82 83 _, err = be.container.CreateIfNotExists(&options) 84 if err != nil { 85 return nil, errors.Wrap(err, "container.CreateIfNotExists") 86 } 87 88 return be, nil 89 } 90 91 // SetListMaxItems sets the number of list items to load per request. 92 func (be *Backend) SetListMaxItems(i int) { 93 be.listMaxItems = i 94 } 95 96 // IsNotExist returns true if the error is caused by a not existing file. 97 func (be *Backend) IsNotExist(err error) bool { 98 debug.Log("IsNotExist(%T, %#v)", err, err) 99 return os.IsNotExist(err) 100 } 101 102 // Join combines path components with slashes. 103 func (be *Backend) Join(p ...string) string { 104 return path.Join(p...) 105 } 106 107 // Location returns this backend's location (the container name). 108 func (be *Backend) Location() string { 109 return be.Join(be.container.Name, be.prefix) 110 } 111 112 // Path returns the path in the bucket that is used for this backend. 113 func (be *Backend) Path() string { 114 return be.prefix 115 } 116 117 // preventCloser wraps an io.Reader to run a function instead of the original Close() function. 118 type preventCloser struct { 119 io.Reader 120 f func() 121 } 122 123 func (wr preventCloser) Close() error { 124 wr.f() 125 return nil 126 } 127 128 // Save stores data in the backend at the handle. 129 func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) { 130 if err := h.Valid(); err != nil { 131 return err 132 } 133 134 objName := be.Filename(h) 135 136 debug.Log("Save %v at %v", h, objName) 137 138 be.sem.GetToken() 139 140 // wrap the reader so that net/http client cannot close the reader, return 141 // the token instead. 142 rd = preventCloser{ 143 Reader: rd, 144 f: func() { 145 debug.Log("Close()") 146 }, 147 } 148 149 debug.Log("InsertObject(%v, %v)", be.container.Name, objName) 150 151 err = be.container.GetBlobReference(objName).CreateBlockBlobFromReader(rd, nil) 152 153 be.sem.ReleaseToken() 154 debug.Log("%v, err %#v", objName, err) 155 156 return errors.Wrap(err, "CreateBlockBlobFromReader") 157 } 158 159 // wrapReader wraps an io.ReadCloser to run an additional function on Close. 160 type wrapReader struct { 161 io.ReadCloser 162 f func() 163 } 164 165 func (wr wrapReader) Close() error { 166 err := wr.ReadCloser.Close() 167 wr.f() 168 return err 169 } 170 171 // Load runs fn with a reader that yields the contents of the file at h at the 172 // given offset. 173 func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error { 174 return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn) 175 } 176 177 func (be *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 178 debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h)) 179 if err := h.Valid(); err != nil { 180 return nil, err 181 } 182 183 if offset < 0 { 184 return nil, errors.New("offset is negative") 185 } 186 187 if length < 0 { 188 return nil, errors.Errorf("invalid length %d", length) 189 } 190 191 objName := be.Filename(h) 192 blob := be.container.GetBlobReference(objName) 193 194 start := uint64(offset) 195 var end uint64 196 197 if length > 0 { 198 end = uint64(offset + int64(length) - 1) 199 } else { 200 end = 0 201 } 202 203 be.sem.GetToken() 204 205 rd, err := blob.GetRange(&storage.GetBlobRangeOptions{Range: &storage.BlobRange{Start: start, End: end}}) 206 if err != nil { 207 be.sem.ReleaseToken() 208 return nil, err 209 } 210 211 closeRd := wrapReader{ 212 ReadCloser: rd, 213 f: func() { 214 debug.Log("Close()") 215 be.sem.ReleaseToken() 216 }, 217 } 218 219 return closeRd, err 220 } 221 222 // Stat returns information about a blob. 223 func (be *Backend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) { 224 debug.Log("%v", h) 225 226 objName := be.Filename(h) 227 blob := be.container.GetBlobReference(objName) 228 229 be.sem.GetToken() 230 err := blob.GetProperties(nil) 231 be.sem.ReleaseToken() 232 233 if err != nil { 234 debug.Log("blob.GetProperties err %v", err) 235 return restic.FileInfo{}, errors.Wrap(err, "blob.GetProperties") 236 } 237 238 fi := restic.FileInfo{ 239 Size: int64(blob.Properties.ContentLength), 240 Name: h.Name, 241 } 242 return fi, nil 243 } 244 245 // Test returns true if a blob of the given type and name exists in the backend. 246 func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) { 247 objName := be.Filename(h) 248 249 be.sem.GetToken() 250 found, err := be.container.GetBlobReference(objName).Exists() 251 be.sem.ReleaseToken() 252 253 if err != nil { 254 return false, err 255 } 256 return found, nil 257 } 258 259 // Remove removes the blob with the given name and type. 260 func (be *Backend) Remove(ctx context.Context, h restic.Handle) error { 261 objName := be.Filename(h) 262 263 be.sem.GetToken() 264 _, err := be.container.GetBlobReference(objName).DeleteIfExists(nil) 265 be.sem.ReleaseToken() 266 267 debug.Log("Remove(%v) at %v -> err %v", h, objName, err) 268 return errors.Wrap(err, "client.RemoveObject") 269 } 270 271 // List runs fn for each file in the backend which has the type t. When an 272 // error occurs (or fn returns an error), List stops and returns it. 273 func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error { 274 debug.Log("listing %v", t) 275 276 prefix, _ := be.Basedir(t) 277 278 // make sure prefix ends with a slash 279 if !strings.HasSuffix(prefix, "/") { 280 prefix += "/" 281 } 282 283 params := storage.ListBlobsParameters{ 284 MaxResults: uint(be.listMaxItems), 285 Prefix: prefix, 286 } 287 288 for { 289 be.sem.GetToken() 290 obj, err := be.container.ListBlobs(params) 291 be.sem.ReleaseToken() 292 293 if err != nil { 294 return err 295 } 296 297 debug.Log("got %v objects", len(obj.Blobs)) 298 299 for _, item := range obj.Blobs { 300 m := strings.TrimPrefix(item.Name, prefix) 301 if m == "" { 302 continue 303 } 304 305 fi := restic.FileInfo{ 306 Name: path.Base(m), 307 Size: item.Properties.ContentLength, 308 } 309 310 if ctx.Err() != nil { 311 return ctx.Err() 312 } 313 314 err := fn(fi) 315 if err != nil { 316 return err 317 } 318 319 if ctx.Err() != nil { 320 return ctx.Err() 321 } 322 323 } 324 325 if obj.NextMarker == "" { 326 break 327 } 328 params.Marker = obj.NextMarker 329 } 330 331 return ctx.Err() 332 } 333 334 // Remove keys for a specified backend type. 335 func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error { 336 return be.List(ctx, t, func(fi restic.FileInfo) error { 337 return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name}) 338 }) 339 } 340 341 // Delete removes all restic keys in the bucket. It will not remove the bucket itself. 342 func (be *Backend) Delete(ctx context.Context) error { 343 alltypes := []restic.FileType{ 344 restic.DataFile, 345 restic.KeyFile, 346 restic.LockFile, 347 restic.SnapshotFile, 348 restic.IndexFile} 349 350 for _, t := range alltypes { 351 err := be.removeKeys(ctx, t) 352 if err != nil { 353 return nil 354 } 355 } 356 357 return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile}) 358 } 359 360 // Close does nothing 361 func (be *Backend) Close() error { return nil }