Documentation
¶
Index ¶
Constants ¶
const MaxFileFieldStringBytes = 8 * 1024
MaxFileFieldStringBytes caps the length of any FileField string field — anything past this is rejected at validation time. A legitimate URL, MIME, or storage ref does not need 8 KB.
const MaxProcessFileSize int64 = 32 << 20 // 32 MiB
MaxProcessFileSize caps the in-memory size of a single upload read by ProcessFileField. The default protects callers that haven't wired a stricter limit elsewhere from unbounded memory consumption — a hostile client can otherwise stream gigabytes into RAM.
Variables ¶
var ( ErrFileFieldURLScheme = errors.New("filefield: URL has unsafe scheme") ErrFileFieldTraversal = errors.New("filefield: contains path traversal") ErrFileFieldMimeUnsafe = errors.New("filefield: MIME type contains unsafe characters") ErrFileFieldSize = errors.New("filefield: size is negative or oversize") ErrFileFieldOversize = errors.New("filefield: field exceeds length limit") ErrFileFieldTooLarge = errors.New("filefield: file exceeds maximum size") ErrFileFieldUnsafeContent = errors.New("filefield: file content is unsafe by default") ErrFileFieldControlBytes = errors.New("filefield: field contains control bytes") )
Validation errors returned by FileField.Validate. Callers can match on these without parsing the message.
Functions ¶
func DeleteFileField ¶
DeleteFileField removes a previously stored file from the storage backend. Returns nil if the FileField is nil or has no storage reference. The ctx parameter should come from the HTTP request.
func GenerateFilePath ¶
GenerateFilePath produces a safe, unique file path for storage. The format is: uploads/{entityName}/{fieldName}/{sanitized_name}_{timestamp}_{rand}{ext} Example: "uploads/posts/avatar/photo_1683398400000000000_3f9a1c2b.png"
The path carries a crypto/rand component in addition to the timestamp so that two uploads of the same filename to the same field never collide — uniqueness must not depend on clock resolution. Without it, two requests landing within the same nanosecond (or the same clock tick on platforms whose clock does not advance every nanosecond) would resolve to the same path and one upload would silently overwrite the other.
Types ¶
type DerivedVariant ¶ added in v0.47.0
type DerivedVariant struct {
// StorageRef is the storage backend key the rendition was saved under.
// It doubles as the URL path, matching FileField.URL/StorageRef.
StorageRef string `json:"storage_ref"`
// MIME is the rendition's content type (e.g. "image/webp").
MIME string `json:"mime"`
// Width and Height are the rendition's pixel dimensions. They feed a
// responsive srcset directly.
Width int `json:"width"`
Height int `json:"height"`
}
DerivedVariant is one stored rendition of an uploaded image — a single width/format pair produced alongside the original.
type FileField ¶
type FileField struct {
// URL is the publicly accessible path or URL to the file.
URL string `json:"url"`
// Filename is the original filename as provided by the client.
Filename string `json:"filename"`
// MimeType is the detected MIME type of the file.
MimeType string `json:"mime_type"`
// Size is the file size in bytes.
Size int64 `json:"size"`
// StorageRef is the storage backend key used to reference this file.
StorageRef string `json:"storage_ref"`
// Image carries renditions and placeholder metadata derived from an
// uploaded image. Nil unless ProcessFileField ran with
// WithImageDeriver.
Image *ImageDerivatives `json:"image,omitempty"`
}
FileField holds metadata about an uploaded file associated with an entity field.
func ProcessFileField ¶
func ProcessFileField(ctx context.Context, store upload.Storage, file interface { Read([]byte) (int, error) }, filename string, entityName, fieldName string, opts ...ProcessOption) (*FileField, error)
ProcessFileField reads a file from the given reader, saves it via the storage backend, and returns a FileField with all metadata. The file is stored at a path generated by GenerateFilePath.
Defaults that protect callers from common upload abuse paths:
- Size is capped at MaxProcessFileSize. Anything larger returns ErrFileFieldTooLarge without buffering the rest of the body.
- Content is scanned for active markup in two tiers: HARD tokens (<script, <svg, <iframe, <html, <!doctype, <object, <embed, <base) are matched anywhere in the body, and SOFT tokens (<img, <?xml, <style, <link, javascript:) only in the leading 512 bytes. Executable magic bytes (MZ for PE, 0x7fELF for ELF, Mach-O headers) are also rejected. The filename extension and any client-supplied Content-Type are ignored — attackers can lie about both.
The ctx parameter should come from the HTTP request so cancellation and deadlines are respected during slow uploads.
func (*FileField) Validate ¶
Validate enforces invariants on a FileField that came from an untrusted source (typically JSON unmarshal in a CRUD body). It is NOT called automatically — callers that accept FileField as request input should call it before persisting or rendering. The constructors in this package (ProcessFileField) already produce valid FileFields.
Rejected inputs:
- URL with javascript:, vbscript:, or data: scheme — would XSS when rendered as href/src by a downstream consumer.
- URL, StorageRef, or Filename containing `..` segments — would escape the storage root on a vulnerable filesystem backend, and Filename reaches `Content-Disposition: attachment; filename=…`.
- Any C0 control byte or DEL in URL, Filename, or StorageRef. These are persisted and later echoed into a response header, an HTML attribute, or a log line, each of which a CR/LF splits. MimeType is covered by its charset filter, which admits no control byte.
- MimeType containing characters outside the MIME-safe set — normal MIME types are `type/subtype` with letters, digits, `+`, `-`, `.`; angle brackets / quotes indicate an XSS attempt.
- Size < 0 — unsigned conventions require non-negative.
- Any string field over MaxFileFieldStringBytes.
type ImageDerivatives ¶ added in v0.47.0
type ImageDerivatives struct {
// Variants are the stored renditions, ascending by width.
Variants []DerivedVariant `json:"variants,omitempty"`
// BlurHash is a ~28-character base83 string. Cheap to store in a
// column; render it with framework/image.BlurHashDataURL.
BlurHash string `json:"blurhash,omitempty"`
// Placeholder is a base64 data: URL (an LQIP). Larger than a BlurHash
// but needs no decode step at render time.
Placeholder string `json:"placeholder,omitempty"`
}
ImageDerivatives holds everything derived from an uploaded image beyond the original bytes: the stored renditions plus whichever low-fidelity placeholder representations were requested.
func (*ImageDerivatives) Validate ¶ added in v0.47.0
func (d *ImageDerivatives) Validate() error
Validate enforces the same invariants on derived references that FileField.Validate enforces on the primary file — they reach the same sinks (an <img src>, a storage delete) and arrive from the same untrusted places once persisted and read back.
type ImageDeriver ¶ added in v0.47.0
type ImageDeriver interface {
// DeriveImage is called with the raw uploaded bytes after the upload
// has passed content sniffing but before ProcessFileField returns.
//
// primaryRef is the storage key the original was saved under;
// implementations derive rendition keys from it so everything for one
// upload lives together. Renditions must be written through store.
DeriveImage(ctx context.Context, store upload.Storage, data []byte, primaryRef string) (*ImageDerivatives, error)
}
ImageDeriver turns uploaded image bytes into stored renditions and placeholder metadata.
It is an interface rather than a direct call into framework/image because this package is a leaf that framework/crud imports: an edge to framework/image would link every image decoder plus the WebP encoder into every application that has a CRUD handler, whether or not it processes images. framework/imagefield provides the implementation, so only applications that ask for it pay for the pipeline.
Scope note: this is the framework's only per-field transform seam, and it is deliberately image-shaped — it runs on write, for schema.Image fields, and its output goes to sibling columns. The general version (write and read transforms for any field, composed per field) is being designed in https://github.com/DonaldMurillo/gofastr/issues/144. If that lands, this interface is a candidate to become one instance of it rather than its own mechanism; until then, resist growing it sideways into a general hook — widening this interface to cover non-image fields would prejudge the harder half of that design (read transforms versus filter/sort/search).
type ProcessOption ¶ added in v0.47.0
type ProcessOption func(*processConfig)
ProcessOption configures ProcessFileField.
func WithImageDeriver ¶ added in v0.47.0
func WithImageDeriver(deriver ImageDeriver) ProcessOption
WithImageDeriver runs deriver over the uploaded bytes and attaches the result to FileField.Image.
A derive failure fails the whole upload rather than yielding a file with no renditions. The caller asked for renditions; silently returning a FileField whose Image is nil would surface much later as a page with no placeholder and no srcset, with nothing in the logs pointing back here. It also means a non-image uploaded to an image field is rejected, which is the desired behavior for a schema.Image column.