media

package
v0.1.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 12 Imported by: 0

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

View Source
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.

View Source
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.

View Source
const MaxUploadBytes = capPDF

MaxUploadBytes is the hard cap across every category, used when sizing the multipart reader. Pick the largest category cap.

Variables

View Source
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")
)
View Source
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.

View Source
var ErrTooLarge = errors.New("file exceeds its size limit")

ErrTooLarge signals an upload that exceeds the per-category cap.

Functions

func CapForMime

func CapForMime(mime string) int64

CapForMime returns the size cap for an already-validated MIME.

func DetectAndValidateMime

func DetectAndValidateMime(head []byte, claimed string) (mime string, ext string, err error)

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

func SlugifyFilename(raw string) string

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

func ValidateFolderSlug(s string) error

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

type Listing struct {
	Folders []Folder `json:"folders"`
	Files   []File   `json:"files"`
}

Listing is what ListChildren returns — folders + files at a single parent level, not recursive.

type Service

type Service struct {
	Store *Store
	Root  string
}

Service is the handler-facing facade. Wires Store + uploads root.

func NewService

func NewService(store *Store, root string) (*Service, error)

NewService prepares the uploads root and returns a Service bound to it.

func (*Service) DeleteFile

func (s *Service) DeleteFile(ownerID, fileID string) error

DeleteFile removes the row and unlinks the bytes on disk.

func (*Service) DeleteFolder

func (s *Service) DeleteFolder(ownerID, folderID string, recursive bool) error

DeleteFolder removes the folder and every descendant (when recursive=true). Unlinks on-disk files for every removed row.

func (*Service) ReadFile

func (s *Service) ReadFile(fileID string) ([]byte, string, error)

ReadFile returns the bytes + stored MIME for a file id.

func (*Service) Resolve

func (s *Service) Resolve(remainingPath string) ([]byte, string, error)

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

func (s *Store) BuildPublicPath(f File) (string, error)

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

func (s *Store) DeleteFile(ownerID, fileID string) (string, error)

DeleteFile drops the row and returns the on-disk path so the caller can unlink. Missing → ErrNotFound.

func (*Store) DeleteFolder

func (s *Store) DeleteFolder(ownerID, folderID string, recursive bool) ([]string, error)

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) GetFile

func (s *Store) GetFile(id string) (File, error)

GetFile returns a file row by id.

func (*Store) GetFolder

func (s *Store) GetFolder(id string) (Folder, error)

GetFolder returns a folder row by id.

func (*Store) InsertFile

func (s *Store) InsertFile(f File) (File, error)

InsertFile persists a freshly-uploaded file row. Enforces uniqueness of (owner_id, folder_id, filename).

func (*Store) InsertFolder

func (s *Store) InsertFolder(ownerID string, parentID *string, slug string) (Folder, error)

InsertFolder creates a new folder. Enforces uniqueness within the parent and the max-depth cap. `parentID` may be nil (root-level).

func (*Store) ListChildren

func (s *Store) ListChildren(ownerID string, parentID *string) (Listing, error)

ListChildren returns the folders + files that sit directly under the given parent (nil = owner root).

func (*Store) ResolveByPath

func (s *Store) ResolveByPath(remainingPath string) (File, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL