stash/pkg/file/file.go

226 lines
4.9 KiB
Go

package file
import (
"bytes"
"context"
"io"
"io/fs"
"net/http"
"strconv"
"time"
)
// ID represents an ID of a file.
type ID int32
func (i ID) String() string {
return strconv.Itoa(int(i))
}
// DirEntry represents a file or directory in the file system.
type DirEntry struct {
ZipFileID *ID `json:"zip_file_id"`
// transient - not persisted
// only guaranteed to have id, path and basename set
ZipFile File
ModTime time.Time `json:"mod_time"`
}
func (e *DirEntry) info(fs FS, path string) (fs.FileInfo, error) {
if e.ZipFile != nil {
zipPath := e.ZipFile.Base().Path
zfs, err := fs.OpenZip(zipPath)
if err != nil {
return nil, err
}
defer zfs.Close()
fs = zfs
}
// else assume os file
ret, err := fs.Lstat(path)
return ret, err
}
// File represents a file in the file system.
type File interface {
Base() *BaseFile
SetFingerprints(fp Fingerprints)
Open(fs FS) (io.ReadCloser, error)
}
// BaseFile represents a file in the file system.
type BaseFile struct {
ID ID `json:"id"`
DirEntry
// resolved from parent folder and basename only - not stored in DB
Path string `json:"path"`
Basename string `json:"basename"`
ParentFolderID FolderID `json:"parent_folder_id"`
Fingerprints Fingerprints `json:"fingerprints"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SetFingerprints sets the fingerprints of the file.
// If a fingerprint of the same type already exists, it is overwritten.
func (f *BaseFile) SetFingerprints(fp Fingerprints) {
for _, v := range fp {
f.SetFingerprint(v)
}
}
// SetFingerprint sets the fingerprint of the file.
// If a fingerprint of the same type already exists, it is overwritten.
func (f *BaseFile) SetFingerprint(fp Fingerprint) {
for i, existing := range f.Fingerprints {
if existing.Type == fp.Type {
f.Fingerprints[i] = fp
return
}
}
f.Fingerprints = append(f.Fingerprints, fp)
}
// Base is used to fulfil the File interface.
func (f *BaseFile) Base() *BaseFile {
return f
}
func (f *BaseFile) Open(fs FS) (io.ReadCloser, error) {
if f.ZipFile != nil {
zipPath := f.ZipFile.Base().Path
zfs, err := fs.OpenZip(zipPath)
if err != nil {
return nil, err
}
return zfs.OpenOnly(f.Path)
}
return fs.Open(f.Path)
}
func (f *BaseFile) Info(fs FS) (fs.FileInfo, error) {
return f.info(fs, f.Path)
}
func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) error {
reader, err := f.Open(fs)
if err != nil {
return err
}
defer reader.Close()
content, ok := reader.(io.ReadSeeker)
if !ok {
data, err := io.ReadAll(reader)
if err != nil {
return err
}
content = bytes.NewReader(data)
}
if r.URL.Query().Has("t") {
w.Header().Set("Cache-Control", "private, max-age=31536000, immutable")
} else {
w.Header().Set("Cache-Control", "no-cache")
}
http.ServeContent(w, r, f.Basename, f.ModTime, content)
return nil
}
type Finder interface {
Find(ctx context.Context, id ...ID) ([]File, error)
}
// Getter provides methods to find Files.
type Getter interface {
Finder
FindByPath(ctx context.Context, path string) (File, error)
FindAllByPath(ctx context.Context, path string) ([]File, error)
FindByFingerprint(ctx context.Context, fp Fingerprint) ([]File, error)
FindByZipFileID(ctx context.Context, zipFileID ID) ([]File, error)
FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]File, error)
FindByFileInfo(ctx context.Context, info fs.FileInfo, size int64) ([]File, error)
}
type Counter interface {
CountAllInPaths(ctx context.Context, p []string) (int, error)
CountByFolderID(ctx context.Context, folderID FolderID) (int, error)
}
// Creator provides methods to create Files.
type Creator interface {
Create(ctx context.Context, f File) error
}
// Updater provides methods to update Files.
type Updater interface {
Update(ctx context.Context, f File) error
}
type Destroyer interface {
Destroy(ctx context.Context, id ID) error
}
type GetterUpdater interface {
Getter
Updater
}
type GetterDestroyer interface {
Getter
Destroyer
}
// Store provides methods to find, create and update Files.
type Store interface {
Getter
Counter
Creator
Updater
Destroyer
IsPrimary(ctx context.Context, fileID ID) (bool, error)
}
// Decorator wraps the Decorate method to add additional functionality while scanning files.
type Decorator interface {
Decorate(ctx context.Context, fs FS, f File) (File, error)
IsMissingMetadata(ctx context.Context, fs FS, f File) bool
}
type FilteredDecorator struct {
Decorator
Filter
}
// Decorate runs the decorator if the filter accepts the file.
func (d *FilteredDecorator) Decorate(ctx context.Context, fs FS, f File) (File, error) {
if d.Accept(ctx, f) {
return d.Decorator.Decorate(ctx, fs, f)
}
return f, nil
}
func (d *FilteredDecorator) IsMissingMetadata(ctx context.Context, fs FS, f File) bool {
if d.Accept(ctx, f) {
return d.Decorator.IsMissingMetadata(ctx, fs, f)
}
return false
}