github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/appfs/copier.go (about)

     1  package appfs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/andybalholm/brotli"
    14  	"github.com/cozy/cozy-stack/pkg/consts"
    15  	"github.com/cozy/cozy-stack/pkg/filetype"
    16  	"github.com/cozy/cozy-stack/pkg/logger"
    17  	"github.com/cozy/cozy-stack/pkg/utils"
    18  	"github.com/ncw/swift/v2"
    19  	"github.com/spf13/afero"
    20  )
    21  
    22  // Copier is an interface defining a common set of functions for the installer
    23  // to copy the application into an unknown storage.
    24  type Copier interface {
    25  	Exist(slug, version, shasum string) (exists bool, err error)
    26  	Start(slug, version, shasum string) (exists bool, err error)
    27  	Copy(stat os.FileInfo, src io.Reader) error
    28  	Abort() error
    29  	Commit() error
    30  }
    31  
    32  type swiftCopier struct {
    33  	c           *swift.Connection
    34  	appObj      string
    35  	tmpObj      string
    36  	container   string
    37  	started     bool
    38  	objectNames []string
    39  	ctx         context.Context
    40  }
    41  
    42  type aferoCopier struct {
    43  	fs      afero.Fs
    44  	appDir  string
    45  	tmpDir  string
    46  	started bool
    47  }
    48  
    49  // NewSwiftCopier defines a Copier storing data into a swift container.
    50  func NewSwiftCopier(conn *swift.Connection, appsType consts.AppType) Copier {
    51  	return &swiftCopier{
    52  		c:         conn,
    53  		container: containerName(appsType),
    54  		ctx:       context.Background(),
    55  	}
    56  }
    57  
    58  func (f *swiftCopier) Exist(slug, version, shasum string) (bool, error) {
    59  	f.appObj = path.Join(slug, version)
    60  	if shasum != "" {
    61  		f.appObj += "-" + shasum
    62  	}
    63  	_, _, err := f.c.Object(f.ctx, f.container, f.appObj)
    64  	if err == nil {
    65  		return true, nil
    66  	}
    67  	if !errors.Is(err, swift.ObjectNotFound) {
    68  		return false, err
    69  	}
    70  	return false, nil
    71  }
    72  
    73  func (f *swiftCopier) Start(slug, version, shasum string) (bool, error) {
    74  	exist, err := f.Exist(slug, version, shasum)
    75  	if err != nil || exist {
    76  		return exist, err
    77  	}
    78  
    79  	if _, _, err = f.c.Container(f.ctx, f.container); errors.Is(err, swift.ContainerNotFound) {
    80  		if err = f.c.ContainerCreate(f.ctx, f.container, nil); err != nil {
    81  			return false, err
    82  		}
    83  	}
    84  	f.tmpObj = "tmp-" + utils.RandomString(20) + "/"
    85  	f.objectNames = []string{}
    86  	f.started = true
    87  	return false, err
    88  }
    89  
    90  func (f *swiftCopier) Copy(stat os.FileInfo, src io.Reader) (err error) {
    91  	if !f.started {
    92  		panic("copier should call Start() before Copy()")
    93  	}
    94  
    95  	objName := path.Join(f.tmpObj, stat.Name())
    96  	objMeta := swift.Metadata{
    97  		"content-encoding":        "br",
    98  		"original-content-length": strconv.FormatInt(stat.Size(), 10),
    99  	}
   100  
   101  	contentType := filetype.ByExtension(path.Ext(stat.Name()))
   102  	if contentType == "" {
   103  		contentType, src = filetype.FromReader(src)
   104  	}
   105  
   106  	f.objectNames = append(f.objectNames, objName)
   107  	file, err := f.c.ObjectCreate(f.ctx, f.container, objName, true, "",
   108  		contentType, objMeta.ObjectHeaders())
   109  	if err != nil {
   110  		return err
   111  	}
   112  	defer func() {
   113  		if errc := file.Close(); errc != nil {
   114  			err = errc
   115  		}
   116  	}()
   117  
   118  	bw := brotli.NewWriter(file)
   119  	_, err = io.Copy(bw, src)
   120  	if errc := bw.Close(); errc != nil && err == nil {
   121  		err = errc
   122  	}
   123  	return err
   124  }
   125  
   126  func (f *swiftCopier) Abort() error {
   127  	_, err := f.c.BulkDelete(f.ctx, f.container, f.objectNames)
   128  	return err
   129  }
   130  
   131  func (f *swiftCopier) Commit() (err error) {
   132  	defer func() {
   133  		_, errc := f.c.BulkDelete(f.ctx, f.container, f.objectNames)
   134  		if errc != nil {
   135  			logger.WithNamespace("appfs").Errorf("Cannot BulkDelete after commit: %s", errc)
   136  		}
   137  	}()
   138  	// We check if the appObj has not been created concurrently by another
   139  	// copier.
   140  	_, _, err = f.c.Object(f.ctx, f.container, f.appObj)
   141  	if err == nil {
   142  		return nil
   143  	}
   144  	for _, srcObjectName := range f.objectNames {
   145  		dstObjectName := path.Join(f.appObj, strings.TrimPrefix(srcObjectName, f.tmpObj))
   146  		_, err = f.c.ObjectCopy(f.ctx, f.container, srcObjectName, f.container, dstObjectName, nil)
   147  		if err != nil {
   148  			logger.WithNamespace("appfs").Errorf("Cannot copy file: %s", err)
   149  			return err
   150  		}
   151  	}
   152  	return f.c.ObjectPutString(f.ctx, f.container, f.appObj, "", "text/plain")
   153  }
   154  
   155  // NewAferoCopier defines a copier using an afero.Fs filesystem to store the
   156  // application data.
   157  func NewAferoCopier(fs afero.Fs) Copier {
   158  	return &aferoCopier{fs: fs}
   159  }
   160  
   161  func (f *aferoCopier) Exist(slug, version, shasum string) (bool, error) {
   162  	appDir := path.Join("/", slug, version)
   163  	if shasum != "" {
   164  		appDir += "-" + shasum
   165  	}
   166  	return afero.DirExists(f.fs, appDir)
   167  }
   168  
   169  func (f *aferoCopier) Start(slug, version, shasum string) (bool, error) {
   170  	f.appDir = path.Join("/", slug, version)
   171  	if shasum != "" {
   172  		f.appDir += "-" + shasum
   173  	}
   174  	exists, err := afero.DirExists(f.fs, f.appDir)
   175  	if err != nil || exists {
   176  		return exists, err
   177  	}
   178  	dir := path.Dir(f.appDir)
   179  	if err = f.fs.MkdirAll(dir, 0755); err != nil {
   180  		return false, err
   181  	}
   182  	f.tmpDir, err = afero.TempDir(f.fs, dir, "tmp")
   183  	if err != nil {
   184  		return false, err
   185  	}
   186  	f.started = true
   187  	return false, nil
   188  }
   189  
   190  func (f *aferoCopier) Copy(stat os.FileInfo, src io.Reader) (err error) {
   191  	if !f.started {
   192  		panic("copier should call Start() before Copy()")
   193  	}
   194  
   195  	fullpath := path.Join(f.tmpDir, stat.Name()) + ".br"
   196  	dir := path.Dir(fullpath)
   197  	if err = f.fs.MkdirAll(dir, 0755); err != nil {
   198  		return err
   199  	}
   200  
   201  	dst, err := f.fs.Create(fullpath)
   202  	if err != nil {
   203  		return err
   204  	}
   205  	defer func() {
   206  		if errc := dst.Close(); errc != nil {
   207  			err = errc
   208  		}
   209  	}()
   210  
   211  	bw := brotli.NewWriter(dst)
   212  	_, err = io.Copy(bw, src)
   213  	if errc := bw.Close(); errc != nil && err == nil {
   214  		err = errc
   215  	}
   216  	return err
   217  }
   218  
   219  func (f *aferoCopier) Commit() error {
   220  	return f.fs.Rename(f.tmpDir, f.appDir)
   221  }
   222  
   223  func (f *aferoCopier) Abort() error {
   224  	return f.fs.RemoveAll(f.tmpDir)
   225  }
   226  
   227  // NewFileInfo returns an os.FileInfo
   228  func NewFileInfo(name string, size int64, mode os.FileMode) os.FileInfo {
   229  	return &fileInfo{
   230  		name: name,
   231  		size: size,
   232  		mode: mode,
   233  	}
   234  }
   235  
   236  type fileInfo struct {
   237  	name string
   238  	size int64
   239  	mode os.FileMode
   240  }
   241  
   242  func (f *fileInfo) Name() string       { return f.name }
   243  func (f *fileInfo) Size() int64        { return f.size }
   244  func (f *fileInfo) Mode() os.FileMode  { return f.mode }
   245  func (f *fileInfo) ModTime() time.Time { return time.Now() }
   246  func (f *fileInfo) IsDir() bool        { return false }
   247  func (f *fileInfo) Sys() interface{}   { return nil }