This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package medialocal stores prepared media in a private content-addressed
|
||||
// filesystem tree.
|
||||
package medialocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gamertan.com/web/media"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
root string
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
func Open(root string, options Options) (*Store, error) {
|
||||
absolute, err := filepath.Abs(root)
|
||||
if err != nil || filepath.Clean(absolute) != absolute {
|
||||
return nil, errors.New("medialocal: root must be a clean absolute path")
|
||||
}
|
||||
if err = secureDirectory(absolute, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(absolute)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("medialocal: resolve root: %w", err)
|
||||
}
|
||||
if options.Now == nil {
|
||||
options.Now = time.Now
|
||||
}
|
||||
return &Store{root: resolved, now: options.Now}, nil
|
||||
}
|
||||
|
||||
func (store *Store) Put(ctx context.Context, prepared media.Prepared) (media.Object, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return media.Object{}, err
|
||||
}
|
||||
if len(prepared.Data) == 0 || !media.ValidKey(prepared.Key()) || sha256.Sum256(prepared.Data) != prepared.Digest {
|
||||
return media.Object{}, media.ErrInvalidMedia
|
||||
}
|
||||
shard, target := store.objectPath(prepared.Key())
|
||||
if err := secureDirectory(shard, true); err != nil {
|
||||
return media.Object{}, err
|
||||
}
|
||||
if object, ok, err := inspect(target, prepared.MediaType); err != nil {
|
||||
return media.Object{}, err
|
||||
} else if ok {
|
||||
if object.Size != int64(len(prepared.Data)) {
|
||||
return media.Object{}, errors.New("medialocal: existing digest has an unexpected size")
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
temporary, err := os.CreateTemp(shard, ".upload-*")
|
||||
if err != nil {
|
||||
return media.Object{}, fmt.Errorf("medialocal: create temporary object: %w", err)
|
||||
}
|
||||
temporaryName := temporary.Name()
|
||||
defer os.Remove(temporaryName)
|
||||
if err = temporary.Chmod(0o640); err == nil {
|
||||
_, err = temporary.Write(prepared.Data)
|
||||
}
|
||||
if err == nil {
|
||||
err = temporary.Sync()
|
||||
}
|
||||
if closeErr := temporary.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return media.Object{}, fmt.Errorf("medialocal: write object: %w", err)
|
||||
}
|
||||
if err = os.Link(temporaryName, target); err != nil {
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
object, ok, inspectErr := inspect(target, prepared.MediaType)
|
||||
if inspectErr != nil {
|
||||
return media.Object{}, inspectErr
|
||||
}
|
||||
if ok && object.Size == int64(len(prepared.Data)) {
|
||||
return object, nil
|
||||
}
|
||||
}
|
||||
return media.Object{}, fmt.Errorf("medialocal: commit object: %w", err)
|
||||
}
|
||||
return media.Object{Key: prepared.Key(), Size: int64(len(prepared.Data)), MediaType: prepared.MediaType, CreatedAt: store.now().UTC()}, nil
|
||||
}
|
||||
|
||||
func (store *Store) Open(ctx context.Context, key string) (io.ReadCloser, media.Object, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, media.Object{}, err
|
||||
}
|
||||
if !media.ValidKey(key) {
|
||||
return nil, media.Object{}, media.ErrNotFound
|
||||
}
|
||||
shard, target := store.objectPath(key)
|
||||
if err := secureDirectory(shard, false); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, media.Object{}, media.ErrNotFound
|
||||
}
|
||||
return nil, media.Object{}, err
|
||||
}
|
||||
before, err := os.Lstat(target)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, media.Object{}, media.ErrNotFound
|
||||
}
|
||||
if err != nil || !before.Mode().IsRegular() || before.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, media.Object{}, errors.New("medialocal: object is not a regular file")
|
||||
}
|
||||
file, err := os.Open(target)
|
||||
if err != nil {
|
||||
return nil, media.Object{}, err
|
||||
}
|
||||
after, err := file.Stat()
|
||||
if err != nil || !os.SameFile(before, after) {
|
||||
file.Close()
|
||||
return nil, media.Object{}, errors.New("medialocal: object changed while opening")
|
||||
}
|
||||
return file, media.Object{Key: key, Size: after.Size(), CreatedAt: after.ModTime().UTC()}, nil
|
||||
}
|
||||
|
||||
func (store *Store) Delete(ctx context.Context, key string) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !media.ValidKey(key) {
|
||||
return media.ErrNotFound
|
||||
}
|
||||
_, target := store.objectPath(key)
|
||||
info, err := os.Lstat(target)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return media.ErrNotFound
|
||||
}
|
||||
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return errors.New("medialocal: refusing to delete a non-regular object")
|
||||
}
|
||||
if err = os.Remove(target); errors.Is(err, os.ErrNotExist) {
|
||||
return media.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *Store) objectPath(key string) (string, string) {
|
||||
shard := filepath.Join(store.root, key[:2])
|
||||
return shard, filepath.Join(shard, key)
|
||||
}
|
||||
|
||||
func secureDirectory(path string, create bool) error {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) && create {
|
||||
if err = os.MkdirAll(path, 0o750); err != nil {
|
||||
return fmt.Errorf("medialocal: create directory: %w", err)
|
||||
}
|
||||
info, err = os.Lstat(path)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return errors.New("medialocal: storage directory must not be a symlink")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inspect(path, mediaType string) (media.Object, bool, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return media.Object{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return media.Object{}, false, err
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return media.Object{}, false, errors.New("medialocal: existing object is not a regular file")
|
||||
}
|
||||
return media.Object{Key: filepath.Base(path), Size: info.Size(), MediaType: mediaType, CreatedAt: info.ModTime().UTC()}, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user