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 }