package rootstore import ( "context" "crypto/rand" "encoding/json" "errors" "io" "io/fs" "net/http" "os" "path/filepath" "github.com/tus/tusd/v2/pkg/handler" ) var ( defaultFilePerm = os.FileMode(0o664) defaultDirectoryPerm = os.FileMode(0o754) ) const ( // StorageKeyPath is the key of the path of uploaded file in handler.FileInfo.Storage. StorageKeyPath = "Path" // StorageKeyInfoPath is the key of the path of .info file in handler.FileInfo.Storage. StorageKeyInfoPath = "InfoPath" ) // RootStore is a file system based data store for tusd. type RootStore struct { root *os.Root } func New(root *os.Root) RootStore { return RootStore{root: root} } // UseIn sets this store as the core data store in the passed composer and adds // all possible extension to it. func (store RootStore) UseIn(composer *handler.StoreComposer) { composer.UseCore(store) composer.UseTerminater(store) composer.UseConcater(store) composer.UseLengthDeferrer(store) composer.UseContentServer(store) } func (store RootStore) NewUpload(_ context.Context, info handler.FileInfo) (handler.Upload, error) { if info.ID == "" { info.ID = rand.Text() } // The .info file's location can directly be deduced from the upload ID infoPath := store.infoPath(info.ID) // The binary file's location might be modified by the pre-create hook. var binPath string if info.Storage != nil && info.Storage[StorageKeyPath] != "" { binPath = info.Storage[StorageKeyPath] } else { binPath = store.defaultBinPath(info.ID) } info.Storage = map[string]string{ "Type": "rootstore", StorageKeyPath: binPath, StorageKeyInfoPath: infoPath, } _ = store.root.MkdirAll(filepath.Dir(binPath), defaultDirectoryPerm) // Create binary file with no content if err := store.root.WriteFile(binPath, nil, defaultFilePerm); err != nil { return nil, err } upload := &fileUpload{ root: store.root, info: info, infoPath: infoPath, binPath: binPath, } // writeInfo creates the file by itself if necessary if err := upload.writeInfo(); err != nil { return nil, err } return upload, nil } func (store RootStore) GetUpload(_ context.Context, id string) (handler.Upload, error) { infoPath := store.infoPath(id) data, err := fs.ReadFile(store.root.FS(), filepath.ToSlash(infoPath)) if err != nil { if os.IsNotExist(err) { // Interpret os.ErrNotExist as 404 Not Found err = handler.ErrNotFound } return nil, err } var info handler.FileInfo if err := json.Unmarshal(data, &info); err != nil { return nil, err } // If the info file contains a custom path to the binary file, we use that. If not, we // fall back to the default value (although the Path property should always be set in recent // tusd versions). var binPath string if info.Storage != nil && info.Storage[StorageKeyPath] != "" { // No filepath.Join here because the joining already happened in NewUpload. Duplicate joining // with relative paths lead to incorrect paths binPath = info.Storage[StorageKeyPath] } else { binPath = store.defaultBinPath(info.ID) } stat, err := store.root.Stat(binPath) if err != nil { if os.IsNotExist(err) { // Interpret os.ErrNotExist as 404 Not Found err = handler.ErrNotFound } return nil, err } info.Offset = stat.Size() return &fileUpload{ root: store.root, info: info, binPath: binPath, infoPath: infoPath, }, nil } func (store RootStore) AsTerminatableUpload(upload handler.Upload) handler.TerminatableUpload { return upload.(*fileUpload) //nolint:forcetypeassert } func (store RootStore) AsLengthDeclarableUpload(upload handler.Upload) handler.LengthDeclarableUpload { return upload.(*fileUpload) //nolint:forcetypeassert } func (store RootStore) AsConcatableUpload(upload handler.Upload) handler.ConcatableUpload { return upload.(*fileUpload) //nolint:forcetypeassert } func (store RootStore) AsServableUpload(upload handler.Upload) handler.ServableUpload { return upload.(*fileUpload) //nolint:forcetypeassert } // defaultBinPath returns the path to the file storing the binary data, if it is // not customized using the pre-create hook. func (store RootStore) defaultBinPath(id string) string { return id } // infoPath returns the path to the .info file storing the file's info. func (store RootStore) infoPath(id string) string { return id + ".info" } type fileUpload struct { root *os.Root // info stores the current information about the upload info handler.FileInfo // infoPath is the path to the .info file infoPath string // binPath is the path to the binary file (which has no extension) binPath string } func (upload *fileUpload) GetInfo(_ context.Context) (handler.FileInfo, error) { return upload.info, nil } func (upload *fileUpload) WriteChunk(_ context.Context, _ int64, src io.Reader) (int64, error) { file, err := upload.root.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) if err != nil { return 0, err } // Avoid the use of defer file.Close() here to ensure no errors are lost // See https://github.com/tus/tusd/issues/698. n, err := io.Copy(file, src) upload.info.Offset += n if err != nil { file.Close() return n, err } return n, file.Close() } func (upload *fileUpload) GetReader(_ context.Context) (io.ReadCloser, error) { return upload.root.Open(upload.binPath) } func (upload *fileUpload) Terminate(_ context.Context) error { // We ignore errors indicating that the files cannot be found because we want // to delete them anyways. The files might be removed by a cron job for cleaning up // or some file might have been removed when tusd crashed during the termination. err := upload.root.Remove(upload.binPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } err = upload.root.Remove(upload.infoPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } return nil } func (upload *fileUpload) ConcatUploads(_ context.Context, uploads []handler.Upload) (err error) { file, err := upload.root.OpenFile(upload.binPath, os.O_WRONLY|os.O_APPEND, defaultFilePerm) if err != nil { return err } defer func() { // Ensure that close error is propagated, if it occurs. // See https://github.com/tus/tusd/issues/698. cerr := file.Close() if err == nil { err = cerr } }() for _, partialUpload := range uploads { if err := partialUpload.(*fileUpload).appendTo(file); err != nil { //nolint:forcetypeassert return err } } return } func (upload *fileUpload) appendTo(file *os.File) error { src, err := upload.root.Open(upload.binPath) if err != nil { return err } if _, err := io.Copy(file, src); err != nil { src.Close() return err } return src.Close() } func (upload *fileUpload) DeclareLength(_ context.Context, length int64) error { upload.info.Size = length upload.info.SizeIsDeferred = false return upload.writeInfo() } // writeInfo updates the entire information. Everything will be overwritten. func (upload *fileUpload) writeInfo() error { data, err := json.Marshal(upload.info) if err != nil { return err } _ = upload.root.MkdirAll(filepath.Dir(upload.infoPath), defaultDirectoryPerm) return upload.root.WriteFile(upload.infoPath, data, defaultFilePerm) } func (upload *fileUpload) FinishUpload(_ context.Context) error { return nil } func (upload *fileUpload) ServeContent(_ context.Context, w http.ResponseWriter, r *http.Request) error { http.ServeFileFS(w, r, upload.root.FS(), filepath.ToSlash(upload.binPath)) return nil }