mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-10 15:15:12 -05:00
202 lines
6.8 KiB
Go
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
|
|
}
|