mirror of
				https://github.com/restic/restic.git
				synced 2025-10-27 03:14:17 +00:00 
			
		
		
		
	 8bfc2519d7
			
		
	
	
		8bfc2519d7
		
	
	
	
	
		
			
			The check is now handled by backend.DefaultLoad. This also guarantees consistent behavior across all backends.
		
			
				
	
	
		
			316 lines
		
	
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package swift
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/md5"
 | |
| 	"encoding/hex"
 | |
| 	"fmt"
 | |
| 	"hash"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"path"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/restic/restic/internal/backend"
 | |
| 	"github.com/restic/restic/internal/backend/layout"
 | |
| 	"github.com/restic/restic/internal/backend/sema"
 | |
| 	"github.com/restic/restic/internal/debug"
 | |
| 	"github.com/restic/restic/internal/errors"
 | |
| 	"github.com/restic/restic/internal/restic"
 | |
| 
 | |
| 	"github.com/cenkalti/backoff/v4"
 | |
| 	"github.com/ncw/swift/v2"
 | |
| )
 | |
| 
 | |
| // beSwift is a backend which stores the data on a swift endpoint.
 | |
| type beSwift struct {
 | |
| 	conn        *swift.Connection
 | |
| 	connections uint
 | |
| 	sem         sema.Semaphore
 | |
| 	container   string // Container name
 | |
| 	prefix      string // Prefix of object names in the container
 | |
| 	layout.Layout
 | |
| }
 | |
| 
 | |
| // ensure statically that *beSwift implements restic.Backend.
 | |
| var _ restic.Backend = &beSwift{}
 | |
| 
 | |
| // Open opens the swift backend at a container in region. The container is
 | |
| // created if it does not exist yet.
 | |
| func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) {
 | |
| 	debug.Log("config %#v", cfg)
 | |
| 
 | |
| 	sem, err := sema.New(cfg.Connections)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	be := &beSwift{
 | |
| 		conn: &swift.Connection{
 | |
| 			UserName:                    cfg.UserName,
 | |
| 			UserId:                      cfg.UserID,
 | |
| 			Domain:                      cfg.Domain,
 | |
| 			DomainId:                    cfg.DomainID,
 | |
| 			ApiKey:                      cfg.APIKey,
 | |
| 			AuthUrl:                     cfg.AuthURL,
 | |
| 			Region:                      cfg.Region,
 | |
| 			Tenant:                      cfg.Tenant,
 | |
| 			TenantId:                    cfg.TenantID,
 | |
| 			TenantDomain:                cfg.TenantDomain,
 | |
| 			TenantDomainId:              cfg.TenantDomainID,
 | |
| 			TrustId:                     cfg.TrustID,
 | |
| 			StorageUrl:                  cfg.StorageURL,
 | |
| 			AuthToken:                   cfg.AuthToken.Unwrap(),
 | |
| 			ApplicationCredentialId:     cfg.ApplicationCredentialID,
 | |
| 			ApplicationCredentialName:   cfg.ApplicationCredentialName,
 | |
| 			ApplicationCredentialSecret: cfg.ApplicationCredentialSecret.Unwrap(),
 | |
| 			ConnectTimeout:              time.Minute,
 | |
| 			Timeout:                     time.Minute,
 | |
| 
 | |
| 			Transport: rt,
 | |
| 		},
 | |
| 		connections: cfg.Connections,
 | |
| 		sem:         sem,
 | |
| 		container:   cfg.Container,
 | |
| 		prefix:      cfg.Prefix,
 | |
| 		Layout: &layout.DefaultLayout{
 | |
| 			Path: cfg.Prefix,
 | |
| 			Join: path.Join,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// Authenticate if needed
 | |
| 	if !be.conn.Authenticated() {
 | |
| 		if err := be.conn.Authenticate(ctx); err != nil {
 | |
| 			return nil, errors.Wrap(err, "conn.Authenticate")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Ensure container exists
 | |
| 	switch _, _, err := be.conn.Container(ctx, be.container); err {
 | |
| 	case nil:
 | |
| 		// Container exists
 | |
| 
 | |
| 	case swift.ContainerNotFound:
 | |
| 		err = be.createContainer(ctx, cfg.DefaultContainerPolicy)
 | |
| 		if err != nil {
 | |
| 			return nil, errors.Wrap(err, "beSwift.createContainer")
 | |
| 		}
 | |
| 
 | |
| 	default:
 | |
| 		return nil, errors.Wrap(err, "conn.Container")
 | |
| 	}
 | |
| 
 | |
| 	return be, nil
 | |
| }
 | |
| 
 | |
| func (be *beSwift) createContainer(ctx context.Context, policy string) error {
 | |
| 	var h swift.Headers
 | |
| 	if policy != "" {
 | |
| 		h = swift.Headers{
 | |
| 			"X-Storage-Policy": policy,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return be.conn.ContainerCreate(ctx, be.container, h)
 | |
| }
 | |
| 
 | |
| func (be *beSwift) Connections() uint {
 | |
| 	return be.connections
 | |
| }
 | |
| 
 | |
| // Location returns this backend's location (the container name).
 | |
| func (be *beSwift) Location() string {
 | |
| 	return be.container
 | |
| }
 | |
| 
 | |
| // Hasher may return a hash function for calculating a content hash for the backend
 | |
| func (be *beSwift) Hasher() hash.Hash {
 | |
| 	return md5.New()
 | |
| }
 | |
| 
 | |
| // HasAtomicReplace returns whether Save() can atomically replace files
 | |
| func (be *beSwift) HasAtomicReplace() bool {
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // Load runs fn with a reader that yields the contents of the file at h at the
 | |
| // given offset.
 | |
| func (be *beSwift) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
 | |
| 	return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn)
 | |
| }
 | |
| 
 | |
| func (be *beSwift) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
 | |
| 	debug.Log("Load %v, length %v, offset %v", h, length, offset)
 | |
| 
 | |
| 	objName := be.Filename(h)
 | |
| 
 | |
| 	headers := swift.Headers{}
 | |
| 	if offset > 0 {
 | |
| 		headers["Range"] = fmt.Sprintf("bytes=%d-", offset)
 | |
| 	}
 | |
| 
 | |
| 	if length > 0 {
 | |
| 		headers["Range"] = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)
 | |
| 	}
 | |
| 
 | |
| 	if _, ok := headers["Range"]; ok {
 | |
| 		debug.Log("Load(%v) send range %v", h, headers["Range"])
 | |
| 	}
 | |
| 
 | |
| 	be.sem.GetToken()
 | |
| 	obj, _, err := be.conn.ObjectOpen(ctx, be.container, objName, false, headers)
 | |
| 	if err != nil {
 | |
| 		debug.Log("  err %v", err)
 | |
| 		be.sem.ReleaseToken()
 | |
| 		return nil, errors.Wrap(err, "conn.ObjectOpen")
 | |
| 	}
 | |
| 
 | |
| 	return be.sem.ReleaseTokenOnClose(obj, nil), nil
 | |
| }
 | |
| 
 | |
| // Save stores data in the backend at the handle.
 | |
| func (be *beSwift) Save(ctx context.Context, h restic.Handle, rd restic.RewindReader) error {
 | |
| 	if err := h.Valid(); err != nil {
 | |
| 		return backoff.Permanent(err)
 | |
| 	}
 | |
| 
 | |
| 	objName := be.Filename(h)
 | |
| 
 | |
| 	debug.Log("Save %v at %v", h, objName)
 | |
| 
 | |
| 	be.sem.GetToken()
 | |
| 	defer be.sem.ReleaseToken()
 | |
| 
 | |
| 	encoding := "binary/octet-stream"
 | |
| 
 | |
| 	debug.Log("PutObject(%v, %v, %v)", be.container, objName, encoding)
 | |
| 	hdr := swift.Headers{"Content-Length": strconv.FormatInt(rd.Length(), 10)}
 | |
| 	_, err := be.conn.ObjectPut(ctx,
 | |
| 		be.container, objName, rd, true, hex.EncodeToString(rd.Hash()),
 | |
| 		encoding, hdr)
 | |
| 	// swift does not return the upload length
 | |
| 	debug.Log("%v, err %#v", objName, err)
 | |
| 
 | |
| 	return errors.Wrap(err, "client.PutObject")
 | |
| }
 | |
| 
 | |
| // Stat returns information about a blob.
 | |
| func (be *beSwift) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
 | |
| 	debug.Log("%v", h)
 | |
| 
 | |
| 	objName := be.Filename(h)
 | |
| 
 | |
| 	be.sem.GetToken()
 | |
| 	defer be.sem.ReleaseToken()
 | |
| 
 | |
| 	obj, _, err := be.conn.Object(ctx, be.container, objName)
 | |
| 	if err != nil {
 | |
| 		debug.Log("Object() err %v", err)
 | |
| 		return restic.FileInfo{}, errors.Wrap(err, "conn.Object")
 | |
| 	}
 | |
| 
 | |
| 	return restic.FileInfo{Size: obj.Bytes, Name: h.Name}, nil
 | |
| }
 | |
| 
 | |
| // Remove removes the blob with the given name and type.
 | |
| func (be *beSwift) Remove(ctx context.Context, h restic.Handle) error {
 | |
| 	objName := be.Filename(h)
 | |
| 
 | |
| 	be.sem.GetToken()
 | |
| 	defer be.sem.ReleaseToken()
 | |
| 
 | |
| 	err := be.conn.ObjectDelete(ctx, be.container, objName)
 | |
| 	debug.Log("Remove(%v) -> err %v", h, err)
 | |
| 	return errors.Wrap(err, "conn.ObjectDelete")
 | |
| }
 | |
| 
 | |
| // List runs fn for each file in the backend which has the type t. When an
 | |
| // error occurs (or fn returns an error), List stops and returns it.
 | |
| func (be *beSwift) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
 | |
| 	debug.Log("listing %v", t)
 | |
| 
 | |
| 	prefix, _ := be.Basedir(t)
 | |
| 	prefix += "/"
 | |
| 
 | |
| 	err := be.conn.ObjectsWalk(ctx, be.container, &swift.ObjectsOpts{Prefix: prefix},
 | |
| 		func(ctx context.Context, opts *swift.ObjectsOpts) (interface{}, error) {
 | |
| 			be.sem.GetToken()
 | |
| 			newObjects, err := be.conn.Objects(ctx, be.container, opts)
 | |
| 			be.sem.ReleaseToken()
 | |
| 
 | |
| 			if err != nil {
 | |
| 				return nil, errors.Wrap(err, "conn.ObjectNames")
 | |
| 			}
 | |
| 			for _, obj := range newObjects {
 | |
| 				m := path.Base(strings.TrimPrefix(obj.Name, prefix))
 | |
| 				if m == "" {
 | |
| 					continue
 | |
| 				}
 | |
| 
 | |
| 				fi := restic.FileInfo{
 | |
| 					Name: m,
 | |
| 					Size: obj.Bytes,
 | |
| 				}
 | |
| 
 | |
| 				err := fn(fi)
 | |
| 				if err != nil {
 | |
| 					return nil, err
 | |
| 				}
 | |
| 
 | |
| 				if ctx.Err() != nil {
 | |
| 					return nil, ctx.Err()
 | |
| 				}
 | |
| 			}
 | |
| 			return newObjects, nil
 | |
| 		})
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return ctx.Err()
 | |
| }
 | |
| 
 | |
| // Remove keys for a specified backend type.
 | |
| func (be *beSwift) removeKeys(ctx context.Context, t restic.FileType) error {
 | |
| 	return be.List(ctx, t, func(fi restic.FileInfo) error {
 | |
| 		return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name})
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // IsNotExist returns true if the error is caused by a not existing file.
 | |
| func (be *beSwift) IsNotExist(err error) bool {
 | |
| 	var e *swift.Error
 | |
| 	return errors.As(err, &e) && e.StatusCode == http.StatusNotFound
 | |
| }
 | |
| 
 | |
| // Delete removes all restic objects in the container.
 | |
| // It will not remove the container itself.
 | |
| func (be *beSwift) Delete(ctx context.Context) error {
 | |
| 	alltypes := []restic.FileType{
 | |
| 		restic.PackFile,
 | |
| 		restic.KeyFile,
 | |
| 		restic.LockFile,
 | |
| 		restic.SnapshotFile,
 | |
| 		restic.IndexFile}
 | |
| 
 | |
| 	for _, t := range alltypes {
 | |
| 		err := be.removeKeys(ctx, t)
 | |
| 		if err != nil {
 | |
| 			return nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	err := be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
 | |
| 	if err != nil && !be.IsNotExist(err) {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Close does nothing
 | |
| func (be *beSwift) Close() error { return nil }
 |