golang-lib/web/file/fs.go

70 lines
2.2 KiB
Go

/*
Package file abstracts non-hierarchical file stores. Each file consists of a
name, a MIME type, a UUID, and data. File names may be duplicate.
TODO: think about name vs. UUID again—should the ``name'' really be the filename
and not the UUID?
*/
package file
import (
"fmt"
"io"
"io/fs"
"net/http"
"strings"
"github.com/google/uuid"
)
// An FS is a file store.
type FS interface {
// Store stores a new file with the given UUID, name, and MIME type.
// Its data is taken from the provided Reader. If it encounters an
// error, it does nothing. Any existing file with the same UUID is
// overwritten.
Store(id uuid.UUID, name, contentType string, data io.Reader) error
// RemoveUUID deletes a file.
RemoveUUID(id uuid.UUID) error
// Open opens a file by its name. If multiple files have the same name,
// it is unspecified which one is opened. This may very well be very
// slow. This is bad. Go away.
Open(name string) (fs.File, error)
// OpenUUID opens a file by its UUID.
OpenUUID(id uuid.UUID) (fs.File, error)
// Check checks the health of the file store. If the file store is not
// healthy, it returns a descriptive error. Otherwise, the file store
// should be usable.
Check() error
}
// StoreFromHTTP stores the first file with given form key from an HTTP
// multipart/form-data request. Its Content-Type header is ignored; the type is
// detected. The file name is the last part of the provided file name not
// containing any slashes or backslashes.
//
// TODO: store all files with the given key instead of just the first one
func StoreFromHTTP(fs FS, r *http.Request, key string) error {
file, fileHeader, err := r.FormFile(key)
if err != nil {
return fmt.Errorf("get file from request: %w", err)
}
defer file.Close()
buf := make([]byte, 512)
n, err := file.Read(buf)
if err != nil && err != io.EOF {
return fmt.Errorf("read from file: %w", err)
}
contentType := http.DetectContentType(buf[:n])
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("seek in file: %w", err)
}
i := strings.LastIndexAny(fileHeader.Filename, "/\\")
// if i == -1 { i = -1 }
return fs.Store(uuid.New(), fileHeader.Filename[i+1:], contentType, file)
}