github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/vfs/vfsswift/thumbs_v3.go (about) 1 package vfsswift 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "io" 10 "net/http" 11 "os" 12 "time" 13 14 "github.com/cozy/cozy-stack/model/vfs" 15 "github.com/cozy/cozy-stack/pkg/consts" 16 "github.com/cozy/cozy-stack/pkg/logger" 17 "github.com/cozy/cozy-stack/pkg/prefixer" 18 "github.com/labstack/echo/v4" 19 "github.com/ncw/swift/v2" 20 ) 21 22 var unixEpochZero = time.Time{} 23 24 // NewThumbsFsV3 creates a new thumb filesystem base on swift. 25 // 26 // This version stores the thumbnails in the same container as the main data 27 // container. 28 func NewThumbsFsV3(c *swift.Connection, db prefixer.Prefixer) vfs.Thumbser { 29 return &thumbsV3{ 30 c: c, 31 container: swiftV3ContainerPrefix + db.DBPrefix(), 32 ctx: context.Background(), 33 } 34 } 35 36 type thumbsV3 struct { 37 c *swift.Connection 38 container string 39 ctx context.Context 40 } 41 42 type thumb struct { 43 io.WriteCloser 44 c *swift.Connection 45 container string 46 name string 47 } 48 49 func (t *thumb) Abort() error { 50 ctx := context.Background() 51 errc := t.WriteCloser.Close() 52 errd := t.c.ObjectDelete(ctx, t.container, t.name) 53 // Create an empty file that indicates that the thumbnail generation has failed 54 _ = t.c.ObjectPutString(ctx, t.container, t.name, "", echo.MIMEOctetStream) 55 if errc != nil { 56 return errc 57 } 58 if errd != nil { 59 return errd 60 } 61 return nil 62 } 63 64 func (t *thumb) Commit() error { 65 return t.WriteCloser.Close() 66 } 67 68 func (t *thumbsV3) CreateThumb(img *vfs.FileDoc, format string) (vfs.ThumbFiler, error) { 69 name := t.makeName(img.ID(), format) 70 objMeta := swift.Metadata{ 71 "file-md5": hex.EncodeToString(img.MD5Sum), 72 } 73 obj, err := t.c.ObjectCreate(t.ctx, t.container, name, true, "", "image/jpeg", objMeta.ObjectHeaders()) 74 if err != nil { 75 if _, _, errc := t.c.Container(t.ctx, t.container); errc == swift.ContainerNotFound { 76 if errc = t.c.ContainerCreate(t.ctx, t.container, nil); errc != nil { 77 return nil, err 78 } 79 } else { 80 return nil, err 81 } 82 } 83 th := &thumb{ 84 WriteCloser: obj, 85 c: t.c, 86 container: t.container, 87 name: name, 88 } 89 return th, nil 90 } 91 92 func (t *thumbsV3) ThumbExists(img *vfs.FileDoc, format string) (bool, error) { 93 name := t.makeName(img.ID(), format) 94 _, headers, err := t.c.Object(t.ctx, t.container, name) 95 if errors.Is(err, swift.ObjectNotFound) { 96 return false, nil 97 } 98 if err != nil { 99 return false, err 100 } 101 meta := headers.ObjectMetadata() 102 if md5 := meta["file-md5"]; md5 != "" { 103 var md5sum []byte 104 md5sum, err = hex.DecodeString(md5) 105 if err == nil && !bytes.Equal(md5sum, img.MD5Sum) { 106 return false, nil 107 } 108 } 109 return true, nil 110 } 111 112 func (t *thumbsV3) RemoveThumbs(img *vfs.FileDoc, formats []string) error { 113 objNames := make([]string, len(formats)) 114 for i, format := range formats { 115 objNames[i] = t.makeName(img.ID(), format) 116 } 117 _, err := t.c.BulkDelete(t.ctx, t.container, objNames) 118 return err 119 } 120 121 func (t *thumbsV3) ServeThumbContent(w http.ResponseWriter, req *http.Request, img *vfs.FileDoc, format string) error { 122 name := t.makeName(img.ID(), format) 123 f, o, err := t.c.ObjectOpen(t.ctx, t.container, name, false, nil) 124 if err != nil { 125 return wrapSwiftErr(err) 126 } 127 defer f.Close() 128 129 ctype := o["Content-Type"] 130 if ctype == echo.MIMEOctetStream { 131 // We have some old images where the thumbnail has not been correctly 132 // saved in Swift. We should delete the thumbnail to allow another try. 133 if l, err := f.Length(t.ctx); err == nil && l > 0 { 134 _ = t.RemoveThumbs(img, vfs.ThumbnailFormatNames) 135 return os.ErrNotExist 136 } 137 // Image magick has failed to generate a thumbnail, and retrying would 138 // be useless. 139 return os.ErrInvalid 140 } 141 142 w.Header().Set("Etag", fmt.Sprintf(`"%s"`, o["Etag"])) 143 w.Header().Set("Content-Type", ctype) 144 http.ServeContent(w, req, name, unixEpochZero, &backgroundSeeker{f}) 145 return nil 146 } 147 148 func (t *thumbsV3) CreateNoteThumb(id, mime, format string) (vfs.ThumbFiler, error) { 149 name := t.makeName(id, format) 150 obj, err := t.c.ObjectCreate(t.ctx, t.container, name, true, "", mime, nil) 151 if err != nil { 152 if _, _, errc := t.c.Container(t.ctx, t.container); errc == swift.ContainerNotFound { 153 if errc = t.c.ContainerCreate(t.ctx, t.container, nil); errc != nil { 154 return nil, err 155 } 156 } else { 157 return nil, err 158 } 159 } 160 th := &thumb{ 161 WriteCloser: obj, 162 c: t.c, 163 container: t.container, 164 name: name, 165 } 166 return th, nil 167 } 168 169 func (t *thumbsV3) OpenNoteThumb(id, format string) (io.ReadCloser, error) { 170 name := t.makeName(id, format) 171 obj, _, err := t.c.ObjectOpen(t.ctx, t.container, name, false, nil) 172 if errors.Is(err, swift.ObjectNotFound) { 173 return nil, os.ErrNotExist 174 } 175 if err != nil { 176 return nil, err 177 } 178 return obj, nil 179 } 180 181 func (t *thumbsV3) RemoveNoteThumb(id string, formats []string) error { 182 objNames := make([]string, len(formats)) 183 for i, format := range formats { 184 objNames[i] = t.makeName(id, format) 185 } 186 _, err := t.c.BulkDelete(t.ctx, t.container, objNames) 187 if err != nil { 188 logger.WithNamespace("vfsswift").Infof("Cannot remove note thumbs: %s", err) 189 } 190 return err 191 } 192 193 func (t *thumbsV3) ServeNoteThumbContent(w http.ResponseWriter, req *http.Request, id string) error { 194 name := t.makeName(id, consts.NoteImageThumbFormat) 195 f, o, err := t.c.ObjectOpen(t.ctx, t.container, name, false, nil) 196 if err != nil { 197 name = t.makeName(id, consts.NoteImageOriginalFormat) 198 f, o, err = t.c.ObjectOpen(t.ctx, t.container, name, false, nil) 199 if err != nil { 200 return wrapSwiftErr(err) 201 } 202 } 203 defer f.Close() 204 205 w.Header().Set("Etag", fmt.Sprintf(`"%s"`, o["Etag"])) 206 w.Header().Set("Content-Type", o["Content-Type"]) 207 http.ServeContent(w, req, name, unixEpochZero, &backgroundSeeker{f}) 208 return nil 209 } 210 211 func (t *thumbsV3) makeName(imgID string, format string) string { 212 return fmt.Sprintf("thumbs/%s-%s", MakeObjectName(imgID), format) 213 } 214 215 // MakeObjectName build the swift object name for a given file document.It 216 // creates a virtual subfolder by splitting the document ID, which should be 32 217 // bytes long, on the 27nth byte. This avoid having a flat hierarchy in swift 218 // with no bound 219 func MakeObjectName(docID string) string { 220 if len(docID) != 32 { 221 return docID 222 } 223 return docID[:22] + "/" + docID[22:27] + "/" + docID[27:] 224 } 225 226 func makeDocID(objName string) string { 227 if len(objName) != 34 { 228 return objName 229 } 230 return objName[:22] + objName[23:28] + objName[29:] 231 } 232 233 func wrapSwiftErr(err error) error { 234 if errors.Is(err, swift.ObjectNotFound) || errors.Is(err, swift.ContainerNotFound) { 235 return os.ErrNotExist 236 } 237 return err 238 } 239 240 type backgroundSeeker struct { 241 *swift.ObjectOpenFile 242 } 243 244 func (f *backgroundSeeker) Seek(offset int64, whence int) (int64, error) { 245 return f.ObjectOpenFile.Seek(context.Background(), offset, whence) 246 }