Kyoo/transcoder/src/storage/filestorage.go
solidDoWant 265386f289 Added support for storing transcoder metadata in S3
Signed-off-by: Fred Heinecke <fred.heinecke@yahoo.com>
2025-05-02 11:38:28 +02:00

202 lines
6.8 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/zoriya/kyoo/transcoder/src/utils"
)
type FileStorageBackend struct {
// Base directory for the storage backend
baseDirectory string
baseDirectoryRoot *os.Root
}
// NewFileStorageBackend creates a new FileStorageBackend with the specified base directory.
func NewFileStorageBackend(baseDirectory string) (*FileStorageBackend, error) {
// Attempt to create the directory if it doesn't exist
// This should be the only filesystem call in this file that does not use os.Root.
// This is to prevent directory traversal attacks when the provided input is untrusted.
if err := os.MkdirAll(baseDirectory, 0770); err != nil {
return nil, fmt.Errorf("failed to create storage base directory: %w", err)
}
root, err := os.OpenRoot(baseDirectory)
if err != nil {
return nil, fmt.Errorf("failed to open storage base directory %q: %w", baseDirectory, err)
}
return &FileStorageBackend{
baseDirectory: baseDirectory,
baseDirectoryRoot: root,
}, nil
}
func (fsb *FileStorageBackend) Close() error {
if fsb.baseDirectoryRoot != nil {
return fsb.baseDirectoryRoot.Close()
}
return nil
}
// DoesItemExist checks if an item exists in the file storage backend.
func (fsb *FileStorageBackend) DoesItemExist(_ context.Context, path string) (bool, error) {
_, err := fsb.baseDirectoryRoot.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("failed to check if item %q exists: %w", path, err)
}
return true, nil
}
// ListItemsWithPrefix returns a list of items in the storage backend that match the given prefix.
func (fsb *FileStorageBackend) ListItemsWithPrefix(_ context.Context, pathPrefix string) ([]string, error) {
var items []string
rootFS := fsb.baseDirectoryRoot.FS()
// This is split so that a smaller subset of files are checked, rather than literally everything under the base directory.
// All matching files for the path prefix will also have the prefixDirPath as their parent directory.
prefixDirPath := filepath.Dir(pathPrefix)
err := fs.WalkDir(rootFS, prefixDirPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// This can happen if prefixDirPath does not exist. The walk function will handle
// checking this.
if os.IsNotExist(err) {
return fs.SkipDir
}
return fmt.Errorf("failed on %q while walking directory %q: %w", path, pathPrefix, err)
}
// If the path does not start with the prefix, skip it.
if !strings.HasPrefix(path, pathPrefix) {
if d.IsDir() {
// Skip directories that do not match the prefix
return fs.SkipDir
}
return nil
}
// Collect matching non-directory items
if !d.IsDir() {
items = append(items, path)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk directory %q: %w", pathPrefix, err)
}
return items, nil
}
// DeleteItem deletes an item from the storage backend. If the item does not exist, it returns nil.
func (fsb *FileStorageBackend) DeleteItem(_ context.Context, path string) error {
err := fsb.baseDirectoryRoot.Remove(path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete item %q: %w", path, err)
}
return nil
}
// DeleteItemsWithPrefix deletes all items in the storage backend that match the given prefix.
// Deletion should be "syncronous" (i.e. the function should block until the write is complete).
func (fsb *FileStorageBackend) DeleteItemsWithPrefix(ctx context.Context, pathPrefix string) error {
// Unfortunately this implementation is needed until https://go-review.googlesource.com/c/go/+/661595 is released.
// The new os.Root type does not yet have a RemoveAll method. The next Go release will have this.
// Once RemoveAll is available, this function can be reduced to a single ReadDir call, a filter, and a RemoveAll call.
// Get all items with the prefix
items, err := fsb.ListItemsWithPrefix(ctx, pathPrefix)
if err != nil {
return fmt.Errorf("failed to list items with prefix %q: %w", pathPrefix, err)
}
// Delete all items. This will leave behind empty directories, but that shouldn't really matter. A future
// implementation that uses os.Root.RemoveAll will handle this.
for _, item := range items {
err = fsb.DeleteItem(ctx, item)
if err != nil {
return fmt.Errorf("failed to delete item %q: %w", item, err)
}
}
return nil
}
// SaveItemWithCallback saves an item to the storage backend. If the item already exists, it overwrites it.
// The writeContents function is called with a writer to write the contents of the item.
func (fsb *FileStorageBackend) SaveItemWithCallback(ctx context.Context, path string, writeContents ContentsWriterCallback) (err error) {
// Open the file for writing
file, err := fsb.openFileForWriting(path)
if err != nil {
return fmt.Errorf("failed to open %q for writing: %w", path, err)
}
defer utils.CleanupWithErr(&err, file.Close, "failed to close file %q", path)
// Write the contents using the provided callback
if err := writeContents(ctx, file); err != nil {
return fmt.Errorf("failed to write contents to file %q: %w", path, err)
}
return nil
}
// SaveItem saves an item to the storage backend. If the item already exists, it overwrites it.
func (fsb *FileStorageBackend) SaveItem(ctx context.Context, path string, contents io.Reader) (err error) {
// Open the file for writing
file, err := fsb.openFileForWriting(path)
if err != nil {
return fmt.Errorf("failed to open %q for writing: %w", path, err)
}
defer utils.CleanupWithErr(&err, file.Close, "failed to close file %q", path)
// Copy the contents to the file
if _, err := io.Copy(file, contents); err != nil {
return fmt.Errorf("failed to copy contents to file %q: %w", path, err)
}
return nil
}
// openFileForWriting opens a file for writing. If the file already exists, it overwrites it.
// The parent directory is created if it doesn't exist.
// This function is used internally to create files in the storage backend.
// The returned file should be closed by the caller.
func (fsb *FileStorageBackend) openFileForWriting(path string) (*os.File, error) {
// Create the parent directory if it doesn't exist
dir := filepath.Dir(path)
if err := fsb.baseDirectoryRoot.Mkdir(dir, 0770); err != nil {
return nil, fmt.Errorf("failed to create directory %q: %w", dir, err)
}
// Open the file for writing
file, err := fsb.baseDirectoryRoot.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0660)
if err != nil {
return nil, fmt.Errorf("failed to create file %q: %w", path, err)
}
return file, nil
}
// GetItem retrieves an item from the storage backend.
func (fsb *FileStorageBackend) GetItem(_ context.Context, path string) (io.ReadCloser, error) {
file, err := fsb.baseDirectoryRoot.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open file %q: %w", path, err)
}
return file, nil
}