github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/previewfs/cache.go (about) 1 package previewfs 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "io" 10 "os" 11 "path" 12 "strconv" 13 "time" 14 15 "github.com/cozy/cozy-stack/pkg/config/config" 16 "github.com/ncw/swift/v2" 17 "github.com/spf13/afero" 18 ) 19 20 const ( 21 containerName = "previews" 22 ttl = 30 * 24 * time.Hour 23 ) 24 25 // Cache is a interface for persisting icons & previews of PDF for later reuse. 26 type Cache interface { 27 GetIcon(md5sum []byte) (*bytes.Buffer, error) 28 SetIcon(md5sum []byte, buffer *bytes.Buffer) error 29 GetPreview(md5sum []byte) (*bytes.Buffer, error) 30 SetPreview(md5sum []byte, buffer *bytes.Buffer) error 31 } 32 33 // SystemCache returns the global cache, using the configuration file. 34 func SystemCache() Cache { 35 fsURL := config.FsURL() 36 switch fsURL.Scheme { 37 case config.SchemeFile, config.SchemeMem: 38 fs := afero.NewBasePathFs(afero.NewOsFs(), path.Join(fsURL.Path, containerName)) 39 return aferoCache{fs} 40 case config.SchemeSwift, config.SchemeSwiftSecure: 41 conn := config.GetSwiftConnection() 42 ctx := context.Background() 43 return swiftCache{conn, ctx} 44 default: 45 panic(fmt.Errorf("previewfs: unknown storage provider %s", fsURL.Scheme)) 46 } 47 } 48 49 type aferoCache struct { 50 fs afero.Fs 51 } 52 53 func (a aferoCache) GetIcon(md5sum []byte) (*bytes.Buffer, error) { 54 f, err := a.fs.Open(iconFilename(md5sum)) 55 if err != nil { 56 return nil, err 57 } 58 return readClose(f) 59 } 60 61 func (a aferoCache) SetIcon(md5sum []byte, buffer *bytes.Buffer) error { 62 exists, err := afero.DirExists(a.fs, "/") 63 if err != nil || !exists { 64 _ = a.fs.MkdirAll("/", 0700) 65 } 66 f, err := a.fs.OpenFile(iconFilename(md5sum), os.O_CREATE|os.O_WRONLY, 0600) 67 if err != nil { 68 return err 69 } 70 return writeClose(f, buffer) 71 } 72 73 func (a aferoCache) GetPreview(md5sum []byte) (*bytes.Buffer, error) { 74 f, err := a.fs.Open(previewFilename(md5sum)) 75 if err != nil { 76 return nil, err 77 } 78 return readClose(f) 79 } 80 81 func (a aferoCache) SetPreview(md5sum []byte, buffer *bytes.Buffer) error { 82 exists, err := afero.DirExists(a.fs, "/") 83 if err != nil || !exists { 84 _ = a.fs.MkdirAll("/", 0700) 85 } 86 f, err := a.fs.OpenFile(previewFilename(md5sum), os.O_CREATE|os.O_WRONLY, 0600) 87 if err != nil { 88 return err 89 } 90 return writeClose(f, buffer) 91 } 92 93 type swiftCache struct { 94 c *swift.Connection 95 ctx context.Context 96 } 97 98 func (s swiftCache) GetIcon(md5sum []byte) (*bytes.Buffer, error) { 99 f, _, err := s.c.ObjectOpen(s.ctx, containerName, iconFilename(md5sum), false, nil) 100 if err != nil { 101 return nil, err 102 } 103 return readClose(f) 104 } 105 106 func (s swiftCache) SetIcon(md5sum []byte, buffer *bytes.Buffer) error { 107 objectName := iconFilename(md5sum) 108 objectMeta := swift.Metadata{"created-at": time.Now().Format(time.RFC3339)} 109 headers := objectMeta.ObjectHeaders() 110 headers["X-Delete-After"] = strconv.FormatInt(int64(ttl.Seconds()), 10) 111 f, err := s.c.ObjectCreate(s.ctx, containerName, objectName, true, "", "image/jpg", headers) 112 if err != nil { 113 return err 114 } 115 err = writeClose(f, buffer) 116 if errors.Is(err, swift.ContainerNotFound) || errors.Is(err, swift.ObjectNotFound) { 117 _ = s.c.ContainerCreate(s.ctx, containerName, nil) 118 f, err = s.c.ObjectCreate(s.ctx, containerName, objectName, true, "", "image/jpg", headers) 119 if err == nil { 120 err = writeClose(f, buffer) 121 } 122 } 123 return err 124 } 125 126 func (s swiftCache) GetPreview(md5sum []byte) (*bytes.Buffer, error) { 127 f, _, err := s.c.ObjectOpen(s.ctx, containerName, previewFilename(md5sum), false, nil) 128 if err != nil { 129 return nil, err 130 } 131 return readClose(f) 132 } 133 134 func (s swiftCache) SetPreview(md5sum []byte, buffer *bytes.Buffer) error { 135 objectName := previewFilename(md5sum) 136 objectMeta := swift.Metadata{"created-at": time.Now().Format(time.RFC3339)} 137 headers := objectMeta.ObjectHeaders() 138 headers["X-Delete-After"] = strconv.FormatInt(int64(ttl.Seconds()), 10) 139 f, err := s.c.ObjectCreate(s.ctx, containerName, objectName, true, "", "image/jpg", headers) 140 if err != nil { 141 return err 142 } 143 err = writeClose(f, buffer) 144 if errors.Is(err, swift.ContainerNotFound) || errors.Is(err, swift.ObjectNotFound) { 145 _ = s.c.ContainerCreate(s.ctx, containerName, nil) 146 f, err = s.c.ObjectCreate(s.ctx, containerName, objectName, true, "", "image/jpg", headers) 147 if err == nil { 148 err = writeClose(f, buffer) 149 } 150 } 151 return err 152 } 153 154 func iconFilename(md5sum []byte) string { 155 return "icon-" + hex.EncodeToString(md5sum) + ".jpg" 156 } 157 158 func previewFilename(md5sum []byte) string { 159 return hex.EncodeToString(md5sum) + ".jpg" 160 } 161 162 func readClose(f io.ReadCloser) (*bytes.Buffer, error) { 163 buffer := &bytes.Buffer{} 164 _, err := buffer.ReadFrom(f) 165 if errc := f.Close(); errc != nil && err == nil { 166 return nil, errc 167 } 168 return buffer, err 169 } 170 171 func writeClose(f io.WriteCloser, buffer *bytes.Buffer) error { 172 _, err := f.Write(buffer.Bytes()) 173 if errc := f.Close(); errc != nil && err == nil { 174 err = errc 175 } 176 return err 177 }