github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/move/archiver.go (about)

     1  package move
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"strconv"
    11  	"time"
    12  
    13  	"github.com/cozy/cozy-stack/model/instance"
    14  	"github.com/cozy/cozy-stack/pkg/config/config"
    15  	"github.com/cozy/cozy-stack/pkg/crypto"
    16  	multierror "github.com/hashicorp/go-multierror"
    17  	"github.com/ncw/swift/v2"
    18  	"github.com/spf13/afero"
    19  )
    20  
    21  var (
    22  	archiveMaxAge    = 7 * 24 * time.Hour
    23  	archiveMACConfig = crypto.MACConfig{
    24  		Name:   "exports",
    25  		MaxAge: archiveMaxAge,
    26  		MaxLen: 256,
    27  	}
    28  )
    29  
    30  // Archiver is an interface describing an abstraction for storing archived
    31  // data.
    32  type Archiver interface {
    33  	OpenArchive(inst *instance.Instance, exportDoc *ExportDoc) (io.ReadCloser, error)
    34  	CreateArchive(exportDoc *ExportDoc) (io.WriteCloser, error)
    35  	RemoveArchives(exportDocs []*ExportDoc) error
    36  }
    37  
    38  // SystemArchiver returns the global system archiver, corresponding to the
    39  // user's configuration.
    40  func SystemArchiver() Archiver {
    41  	fsURL := config.FsURL()
    42  	switch fsURL.Scheme {
    43  	case config.SchemeFile, config.SchemeMem:
    44  		fs := afero.NewBasePathFs(afero.NewOsFs(), path.Join(fsURL.Path, "exports"))
    45  		return newAferoArchiver(fs)
    46  	case config.SchemeSwift, config.SchemeSwiftSecure:
    47  		return newSwiftArchiver()
    48  	default:
    49  		panic(fmt.Errorf("exports: unknown storage provider %s", fsURL.Scheme))
    50  	}
    51  }
    52  
    53  func newAferoArchiver(fs afero.Fs) Archiver {
    54  	return aferoArchiver{fs}
    55  }
    56  
    57  type aferoArchiver struct {
    58  	fs afero.Fs
    59  }
    60  
    61  func (a aferoArchiver) fileName(exportDoc *ExportDoc) string {
    62  	return path.Join(exportDoc.Domain, exportDoc.ID()+"tar.gz")
    63  }
    64  
    65  func (a aferoArchiver) OpenArchive(inst *instance.Instance, exportDoc *ExportDoc) (io.ReadCloser, error) {
    66  	return a.fs.Open(a.fileName(exportDoc))
    67  }
    68  
    69  func (a aferoArchiver) CreateArchive(exportDoc *ExportDoc) (io.WriteCloser, error) {
    70  	f, err := a.fs.OpenFile(a.fileName(exportDoc), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
    71  	if os.IsNotExist(err) {
    72  		if err = a.fs.MkdirAll(path.Join("/", exportDoc.Domain), 0700); err == nil {
    73  			f, err = a.fs.OpenFile(a.fileName(exportDoc), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
    74  		}
    75  	}
    76  	return f, err
    77  }
    78  
    79  func (a aferoArchiver) RemoveArchives(exportDocs []*ExportDoc) error {
    80  	var errm error
    81  	for _, e := range exportDocs {
    82  		if err := a.fs.Remove(a.fileName(e)); err != nil {
    83  			errm = multierror.Append(errm, err)
    84  		}
    85  	}
    86  	return errm
    87  }
    88  
    89  func newSwiftArchiver() Archiver {
    90  	return &switfArchiver{
    91  		c:         config.GetSwiftConnection(),
    92  		container: "exports",
    93  		ctx:       context.Background(),
    94  	}
    95  }
    96  
    97  type switfArchiver struct {
    98  	c         *swift.Connection
    99  	container string
   100  	ctx       context.Context
   101  }
   102  
   103  func (a *switfArchiver) init() error {
   104  	if _, _, err := a.c.Container(a.ctx, a.container); errors.Is(err, swift.ContainerNotFound) {
   105  		if err = a.c.ContainerCreate(a.ctx, a.container, nil); err != nil {
   106  			return err
   107  		}
   108  	}
   109  	return nil
   110  }
   111  
   112  func (a *switfArchiver) OpenArchive(inst *instance.Instance, exportDoc *ExportDoc) (io.ReadCloser, error) {
   113  	if err := a.init(); err != nil {
   114  		return nil, err
   115  	}
   116  	objectName := exportDoc.Domain + "/" + exportDoc.ID()
   117  	f, _, err := a.c.ObjectOpen(a.ctx, a.container, objectName, false, nil)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	return f, nil
   122  }
   123  
   124  func (a *switfArchiver) CreateArchive(exportDoc *ExportDoc) (io.WriteCloser, error) {
   125  	if err := a.init(); err != nil {
   126  		return nil, err
   127  	}
   128  	objectName := exportDoc.Domain + "/" + exportDoc.ID()
   129  	objectMeta := swift.Metadata{
   130  		"created-at": exportDoc.CreatedAt.Format(time.RFC3339),
   131  	}
   132  	headers := objectMeta.ObjectHeaders()
   133  	headers["X-Delete-At"] = strconv.FormatInt(exportDoc.ExpiresAt.Unix(), 10)
   134  	return a.c.ObjectCreate(a.ctx, a.container, objectName, true, "",
   135  		"application/tar+gzip", headers)
   136  }
   137  
   138  func (a *switfArchiver) RemoveArchives(exportDocs []*ExportDoc) error {
   139  	if err := a.init(); err != nil {
   140  		return err
   141  	}
   142  	var objectNames []string
   143  	for _, e := range exportDocs {
   144  		objectNames = append(objectNames, e.Domain+"/"+e.ID())
   145  	}
   146  	if len(objectNames) > 0 {
   147  		_, err := a.c.BulkDelete(a.ctx, a.container, objectNames)
   148  		return err
   149  	}
   150  	return nil
   151  }