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  }