Documentation
¶
Overview ¶
Package media implements owner-scoped file uploads organised in folders and served via public URLs. It is framework-level and owns no HTTP routes — callers register their own handlers that delegate to this package's Store + Service.
"Owner" is opaque from this package's point of view — it's just a UUID string that discriminates whose folders/files these are. Applications typically use an application id; other projects may use a workspace id, organisation id, or anything else.
Index ¶
- Constants
- Variables
- func CapForMime(mime string) int64
- func DetectAndValidateMime(head []byte, claimed string) (mime string, ext string, err error)
- func SlugifyFilename(raw string) string
- func ValidateFolderSlug(s string) error
- type File
- type Folder
- type Listing
- type Service
- func (s *Service) DeleteFile(ownerID, fileID string) error
- func (s *Service) DeleteFolder(ownerID, folderID string, recursive bool) error
- func (s *Service) ReadFile(fileID string) ([]byte, string, error)
- func (s *Service) Resolve(remainingPath string) ([]byte, string, error)
- func (s *Service) StoreUpload(ownerID string, folderID *string, rawFilename string, claimedMime string, ...) (File, error)
- type Store
- func (s *Store) BuildPublicPath(f File) (string, error)
- func (s *Store) DeleteFile(ownerID, fileID string) (string, error)
- func (s *Store) DeleteFolder(ownerID, folderID string, recursive bool) ([]string, error)
- func (s *Store) GetFile(id string) (File, error)
- func (s *Store) GetFolder(id string) (Folder, error)
- func (s *Store) InsertFile(f File) (File, error)
- func (s *Store) InsertFolder(ownerID string, parentID *string, slug string) (Folder, error)
- func (s *Store) ListChildren(ownerID string, parentID *string) (Listing, error)
- func (s *Store) ResolveByPath(remainingPath string) (File, error)
Constants ¶
const ContextBagKeyService = "coco.media.Service"
ContextBagKeyService is the DI key under which the Service lives. Callers resolve it via their DI bag when servicing HTTP requests.
const MaxFolderDepth = 5
MaxFolderDepth caps how deep the folder tree can nest. Counts starting at 1 for root-level folders; `a/b/c/d/e` hits the limit.
const MaxUploadBytes = capPDF
MaxUploadBytes is the hard cap across every category, used when sizing the multipart reader. Pick the largest category cap.
Variables ¶
var ( ErrNotFound = errors.New("media: not found") ErrSlugTaken = errors.New("media: name already taken") ErrNameTaken = errors.New("media: filename already taken in this folder") ErrTooDeep = errors.New("media: folder nesting exceeds the maximum depth") ErrNotEmpty = errors.New("media: folder is not empty") )
var ErrMimeNotAllowed = errors.New("unsupported file type; allowed: PNG, JPEG, WebP, GIF, CSS, WOFF/WOFF2/TTF/OTF, PDF")
ErrMimeNotAllowed signals an upload whose sniffed or claimed MIME is outside the allow-list.
var ErrTooLarge = errors.New("file exceeds its size limit")
ErrTooLarge signals an upload that exceeds the per-category cap.
Functions ¶
func CapForMime ¶
CapForMime returns the size cap for an already-validated MIME.
func DetectAndValidateMime ¶
DetectAndValidateMime sniffs the head of the upload, refuses it when the result (or the client-declared MIME) is outside the allow-list, and returns the canonical MIME we'll store. Returns the extension too so the caller can name the on-disk file.
func SlugifyFilename ¶
SlugifyFilename turns an admin-uploaded filename into a storable one. Lowercases, replaces runs of disallowed characters with dashes, preserves a trailing `.<ext>` when present. Falls back to a random name when the input is empty or unfixable.
func ValidateFolderSlug ¶
ValidateFolderSlug checks a single-segment folder name. Trims whitespace, lowercases, and runs the regex. Returns nil when ok.
Types ¶
type File ¶
type File struct {
ID string `json:"id"`
OwnerID string `json:"owner_id"`
FolderID *string `json:"folder_id"`
Filename string `json:"filename"`
MimeType string `json:"mime_type"`
SizeBytes int64 `json:"size_bytes"`
OnDiskPath string `json:"-"`
PublicPath string `json:"public_path,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
File is a stored upload. OnDiskPath is relative to the uploads root and is never leaked to clients. PublicPath is populated on listing so callers can build URLs like `<base>/public/media/<public_path>` without another round-trip to resolve folder slugs.
type Folder ¶
type Folder struct {
ID string `json:"id"`
OwnerID string `json:"owner_id"`
ParentID *string `json:"parent_id"`
Slug string `json:"slug"`
CreatedAt time.Time `json:"created_at"`
}
Folder is a metadata-only node. ParentID is nil for root-level folders (children of the owner, not of another folder).
type Listing ¶
Listing is what ListChildren returns — folders + files at a single parent level, not recursive.
type Service ¶
Service is the handler-facing facade. Wires Store + uploads root.
func NewService ¶
NewService prepares the uploads root and returns a Service bound to it.
func (*Service) DeleteFile ¶
DeleteFile removes the row and unlinks the bytes on disk.
func (*Service) DeleteFolder ¶
DeleteFolder removes the folder and every descendant (when recursive=true). Unlinks on-disk files for every removed row.
func (*Service) Resolve ¶
Resolve implements the fileserver.Resolver interface — the Service doubles as a file-server resolver so callers can plug it straight into a `/public/media/**` route without writing an adapter. `remainingPath` looks like `<ownerID>/<folder>/.../<filename>`.
func (*Service) StoreUpload ¶
func (s *Service) StoreUpload( ownerID string, folderID *string, rawFilename string, claimedMime string, data []byte, ) (File, error)
StoreUpload validates MIME + size, slugifies the filename, writes the bytes to disk, and records the row. On any post-disk-write failure the file is unlinked so we don't leak orphans.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func NewStore ¶
func NewStore(dbm *orm.DatabaseManager) *Store
func (*Store) BuildPublicPath ¶
BuildPublicPath returns the `ownerID/folder/…/filename` string for a file, suitable for appending to the file-server URL prefix. Walks the folder chain to collect slugs.
func (*Store) DeleteFile ¶
DeleteFile drops the row and returns the on-disk path so the caller can unlink. Missing → ErrNotFound.
func (*Store) DeleteFolder ¶
DeleteFolder drops a folder. When recursive=true, also purges every file + descendant folder inside. Returns the list of on-disk paths the caller should unlink. When recursive=false and the folder has any children, returns ErrNotEmpty.
func (*Store) InsertFile ¶
InsertFile persists a freshly-uploaded file row. Enforces uniqueness of (owner_id, folder_id, filename).
func (*Store) InsertFolder ¶
InsertFolder creates a new folder. Enforces uniqueness within the parent and the max-depth cap. `parentID` may be nil (root-level).
func (*Store) ListChildren ¶
ListChildren returns the folders + files that sit directly under the given parent (nil = owner root).
func (*Store) ResolveByPath ¶
ResolveByPath walks `remainingPath` (e.g. "<ownerID>/<folder>/…/ <filename>") down the folder tree and returns the matching file row. First segment is always the owner id. Missing folders or files return ErrNotFound.