Files
catalyst/app/rootstore/rootstore.go
2025-09-21 09:47:29 +00:00

291 lines
7.4 KiB
Go

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
}