Documentation
¶
Overview ¶
Package share manages public download tokens with optional PIN protection, expiry, and download caps.
Index ¶
- Constants
- Variables
- func ClampExpiry(requested *time.Time, maxDays int, now time.Time) (*time.Time, bool)
- func CookieName(token string) string
- func GalleryEligible(entries []FolderEntry) bool
- func MimeForName(name string) string
- func NewToken() (string, error)
- func RenderFolderPage(w http.ResponseWriter, d FolderPageData) error
- func TokenHash(token string) string
- func UnlockTTL() time.Duration
- type CreateOpts
- type FolderEntry
- type FolderEntryKind
- type FolderPageData
- type FolderPageEntry
- type Service
- func (s *Service) AttachSecret(key string)
- func (s *Service) CheckPIN(ctx context.Context, sh *model.Share, pin string) error
- func (s *Service) Cleanup(ctx context.Context) error
- func (s *Service) CountOverMaxTTL(ctx context.Context, now time.Time) (int, error)
- func (s *Service) Create(ctx context.Context, opts CreateOpts) (*model.Share, error)
- func (s *Service) Delete(ctx context.Context, id int64) error
- func (s *Service) IncrementDownload(ctx context.Context, id int64) error
- func (s *Service) IncrementUpload(ctx context.Context, id int64, n int) error
- func (s *Service) ListByNode(ctx context.Context, nodeID int64) ([]*model.Share, error)
- func (s *Service) MaxTTLDays(ctx context.Context) int
- func (s *Service) MintUnlock(token string) string
- func (s *Service) ReleaseDownload(ctx context.Context, id int64) error
- func (s *Service) ReserveDownload(ctx context.Context, id int64) (bool, error)
- func (s *Service) Resolve(ctx context.Context, token, pin string) (*model.Share, error)
- func (s *Service) RevealPIN(sh *model.Share) (string, error)
- func (s *Service) VerifyUnlock(token, value string) bool
- type Viewer
Constants ¶
const AuditActionPinReveal = "share.pin_revealed"
AuditActionPinReveal is the audit row every PIN read writes — success or refusal-after-authorization alike, from the "My shares" endpoint and from an app plugin's share_pin alike. One spelling, so an operator grepping the audit table finds every reveal on the instance.
const DefaultMaxTTLDays = 7
DefaultMaxTTLDays applies when the setting has never been written. Seven days: a link that is meant to be opened this week, and a folder archive that does not have to be kept warm for a month.
const MaxTTLDaysLimit = 3650
MaxTTLDaysLimit bounds the setting itself.
const SettingKeyMaxTTLDays = "share.max_ttl_days"
SettingKeyMaxTTLDays is the settings row holding the longest life a NEW share link may be given, in days. Absent = DefaultMaxTTLDays; "0" = no ceiling. Written by the admin Protection page (PATCH /api/admin/protection {share_max_ttl_days}) and seeded once from FILEX_SHARE_MAX_TTL.
Variables ¶
var ( // ErrPinRequired means the link has a PIN and none was supplied. ErrPinRequired = errors.New("share: pin required") // ErrLocked means too many wrong PINs; the gate is shut for a while. ErrLocked = errors.New("share: too many wrong pins") )
PIN gate errors.
var ( // ErrNoPIN means the link has no PIN at all — there is nothing to show. ErrNoPIN = errors.New("share: link has no pin") // ErrPinNotRecoverable means THIS link's PIN was never sealed: it was // minted before 00049, or while the instance had no key, or it was sealed // under a key that has since been rotated away. The link still works; its // PIN is simply gone, exactly as it was for every link before this. ErrPinNotRecoverable = errors.New("share: pin was not stored recoverably") // ErrNoSecretKey means this INSTANCE has no FILEX_SECRET_KEY, so no PIN it // mints can ever be shown again. An operator can fix this one; the two // above they cannot, and telling them apart is the whole point. ErrNoSecretKey = errors.New("share: no secret key configured") )
Reasons a PIN cannot be shown. They are separate errors because the sentence a person has to read differs for each, and one "not available" for all three would send an operator looking for the wrong fault.
var ErrBadPIN = errors.New("share: bad pin")
ErrBadPIN is returned when an incorrect PIN is supplied.
var ErrExpired = errors.New("share: expired")
ErrExpired is returned by Resolve when a share is past its TTL or download cap.
Functions ¶
func ClampExpiry ¶
ClampExpiry applies the max-TTL ceiling to a requested expiry: nil (never) becomes now+max, a later date is pulled back to now+max, anything within the ceiling is returned as is. The bool reports whether the request was changed, so the caller can tell the user the link's real life. With no ceiling (maxDays == 0) the request is returned untouched.
func CookieName ¶
CookieName is the per-link cookie name, so unlocking one link never unlocks another and a browser can hold several at once.
func GalleryEligible ¶
func GalleryEligible(entries []FolderEntry) bool
GalleryEligible reports whether the browse page should use the gallery layout: at least one visual file AND ≥60% of the FILES (dirs excluded) are images/videos.
func MimeForName ¶
MimeForName resolves a Content-Type for the public sub-file endpoint by extension, defaulting to application/octet-stream.
func NewToken ¶
NewToken mints a share token in the shape every link carries: 32 lowercase hex characters from crypto/rand.
Exported because a caller sometimes has to KNOW a link's token before the row that carries it can exist — see CreateOpts.Token. Nothing else should need it: Create mints its own.
func RenderFolderPage ¶
func RenderFolderPage(w http.ResponseWriter, d FolderPageData) error
RenderFolderPage writes the browse page HTML.
Types ¶
type CreateOpts ¶
type CreateOpts struct {
NodeID int64
PIN string // optional, hashed before persist
ExpiresAt *time.Time // optional
MaxDownloads *int // optional
CreatedBy *int64 // user ID
CreatedVia string // token username of the creating API call (display-only)
// Drop-link options (Kind == model.ShareKindDrop). Ignored/zero for a
// normal download share.
Kind string // "" defaults to download
MaxUploads *int // cap on total files a drop link may receive
DropSettings *string // JSON limits blob
// App-plugin page options (migration 00046). Set only by the
// `share_create` host function: an app's public page IS a share, so it is
// minted here and inherits the expiry ceiling, the PIN gate, the visit
// counter and the administrator's revoke like every other link. Zero here
// means an ordinary share.
PluginID int64
PageID string
Subject string
StateJSON string // the plugin's own durable record (≤ 64 KiB)
FilesJSON string // [{ref,name,file,size,mime}] — the exposed copies
// PurposeJSON is what the app says this link IS (wire.PagePurpose,
// migration 00052) — how a list of links names it.
PurposeJSON string
// Token is the link's token when the caller ALREADY MINTED one and has
// handed it out. Empty — every other caller — mints a fresh one here.
//
// ⚠ It exists for one case: a plugin may promise a share of a file its
// own job is still writing (wasmplugin/public.go). Such a row cannot be
// written until the bytes are committed and the node exists, but the
// plugin needs the link NOW, to put in the mail it is composing. So the
// token is minted with NewToken at the promise and spent here when the
// output lands. Refused unless it has the shape every token has, so a
// caller cannot choose a guessable one.
Token string
}
CreateOpts is the set of fields a caller may supply when minting a share.
type FolderEntry ¶
type FolderEntry struct {
Name string
RelPath string // slash-relative path under the shared root
Kind FolderEntryKind
Size int64
Mtime time.Time
}
FolderEntry is one classified row of a shared folder listing.
type FolderEntryKind ¶
type FolderEntryKind string
FolderEntryKind classifies one listed entry of a shared folder.
const ( EntryDir FolderEntryKind = "dir" EntryImage FolderEntryKind = "image" EntryVideo FolderEntryKind = "video" EntryFile FolderEntryKind = "file" )
Folder entry kinds.
func ClassifyEntry ¶
func ClassifyEntry(name string, isDir bool) FolderEntryKind
ClassifyEntry buckets a name into dir / image / video / other-file by extension (public pages have no DB mime handy — driver listings often don't populate mime either, mirroring the thumb pipeline's fallback).
type FolderPageData ¶
type FolderPageData struct {
Style template.HTML
// Brand is the optional branding header (logo + display name) rendered
// above the card — injected by the handler like Style/Footer, empty when
// the instance has no branding configured. /* wiring:e1 */
Brand template.HTML
Name string // shared folder display name
SubPath string // rel subdir being browsed ("" = share root)
UpHref string // one-level-up href ("" hides the button)
ZipHref string // "download all" target
Gallery bool
DirCount int
FileCnt int
Entries []FolderPageEntry
// Lang / T carry the resolved language and its string table. The handler
// owns locale resolution (it is the one that can see the request); this
// package only renders what it is given, so the folder page can never end
// up in a different language from the PIN gate in front of it.
Lang string
T map[string]string
// CountsLabel is pre-formatted by the handler: "3 klasör · 12 dosya" and
// "3 folders · 12 files" do not share a word order, so a template that
// glued numbers to nouns could only ever be right in one language.
CountsLabel string
}
FolderPageData feeds the folder browse template. Style and Footer are injected by the handler (single-source public page chrome).
func (FolderPageData) Dir ¶
func (d FolderPageData) Dir() string
Dir is the page's `dir`, derived from Lang by the one list (wire.IsRTL): a right-to-left language lays the folder page out right to left.
type FolderPageEntry ¶
type FolderPageEntry struct {
Name string
Href string // open target (dir → ?dir=…, file → f/…)
ThumbSrc string // non-empty → gallery tile renders an <img>
Kind FolderEntryKind
SizeLabel string
DateLabel string
}
FolderPageEntry is a display-ready row/tile for the template.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service provides high-level share operations.
func NewService ¶
NewService constructs a share Service.
The PIN box starts KEYLESS. secretbox.New("") returns a Box that refuses to Seal (ErrNoKey) rather than one that quietly stores plaintext, so a Service nobody called AttachSecret on mints links whose PIN can never be shown — which is exactly the truth about such an instance, and is said in those words by the endpoint rather than guessed at by the screen.
func (*Service) AttachSecret ¶
AttachSecret gives the unlock cookie its signing key — the instance secret (FILEX_SECRET_KEY). Without it a per-process random key is used, which is correct but logs every visitor out when the server restarts and cannot work across two instances behind one address.
⚠ Since 00049 the SAME key also seals the PIN itself (Create → pin_enc, RevealPIN below). Deliberately the same key and the same box as the rest of filex: internal/secretbox is the instance's one recoverable-secret scheme, and a second key here would be a second thing to lose.
func (*Service) CheckPIN ¶
CheckPIN verifies a PIN against a link, counting the misses.
ErrLocked while the gate is shut, ErrBadPIN on a miss (counted), nil on a hit (counter reset). A link with no PIN always passes.
⚠ The lock is checked BEFORE the hash comparison and the RIGHT PIN during a lock is refused too: a lock the correct answer lifts is no lock at all against somebody working through the space — they would simply find it.
⚠ The counters are written through UpdateSharePinLock, which touches only those two columns. A whole-row save here would let a wrong guess carry a stale expiry, cap or exposed-file list back into the database.
func (*Service) CountOverMaxTTL ¶
CountOverMaxTTL counts the EXISTING live links that outlive the current ceiling — no expiry at all, or an expiry later than now+max. They are reported, never modified: the ceiling is a rule for new links, and a customer's link that was minted under the old rule keeps working. The number is what an operator needs to decide whether to revoke any by hand. 0 when there is no ceiling.
func (*Service) Create ¶
Create issues a fresh share token.
The link's expiry is clamped to the max-TTL setting (see ClampExpiry): a request with no expiry gets one, a request past the ceiling is shortened. The returned Share carries the expiry that was actually stored. Links that already exist are never touched by this — only new ones.
func (*Service) IncrementDownload ¶
IncrementDownload bumps the counter — caller decides whether to call before or after streaming the file.
⚠ Not the cap's enforcement point. Serving first and counting afterwards leaves a window in which every concurrent request reads the same pre-download count and is waved through: measured on fm.example.com, a link capped at ONE download handed three complete files to three overlapping clients. Anything that is about to hand bytes to a visitor must call ReserveDownload instead.
func (*Service) IncrementUpload ¶
IncrementUpload bumps a drop link's received-file counter by n. Feeds the MaxUploads cap enforced by Share.IsExpired.
func (*Service) ListByNode ¶
ListByNode returns all shares pointing at a given node.
func (*Service) MaxTTLDays ¶
MaxTTLDays returns the configured ceiling on a new link's life in days (0 = none). A missing or unparsable setting is the default, never "no ceiling": a typo in the settings table must not quietly mint permanent links.
func (*Service) MintUnlock ¶
MintUnlock returns the cookie value proving this browser answered the PIN.
func (*Service) ReleaseDownload ¶
ReleaseDownload returns a reserved slot. Call it ONLY when the serve failed before any byte reached the client — a partial download is still a download.
func (*Service) ReserveDownload ¶
ReserveDownload claims one download against the link's cap BEFORE the bytes are served, and reports whether it got one. False means the cap is spent and the caller must serve nothing.
The context is detached from the request on purpose: the claim is the record that a file was handed out, and a client that hangs up mid-request must not be able to make that record disappear.
func (*Service) Resolve ¶
Resolve looks up a share by token and applies expiry/PIN checks.
pin may be empty when the share has no PIN configured. Returns ErrExpired or ErrBadPIN as appropriate.
func (*Service) RevealPIN ¶
RevealPIN returns a link's PIN in plain, or says why it cannot.
⚠ This function performs NO authorization. It is called from the one endpoint that has already decided the caller is the creator or an admin and that writes the audit row whatever the answer is. Keeping the two apart is deliberate: a permission check buried inside a decrypt helper is one the next caller of that helper silently inherits — or silently skips.
func (*Service) VerifyUnlock ¶
VerifyUnlock checks a cookie value against a link.
type Viewer ¶
type Viewer struct {
Service *Service
Store db.Store
// StorageResolver returns a constructed storage.Driver for a given
// storage ID. Wired by the server.
StorageResolver func(int64) (storage.Driver, error)
}
Viewer renders the public file at /s/{token} (or the metadata at /api/files/share/{token}).
On GET with PIN-protected shares, the controller expects ?pin= in the querystring (or the X-Filex-Pin header).
func (*Viewer) HandleDownload ¶
func (v *Viewer) HandleDownload(w http.ResponseWriter, r *http.Request)
HandleDownload streams the shared file (or returns inline preview).
func (*Viewer) HandleMetadata ¶
func (v *Viewer) HandleMetadata(w http.ResponseWriter, r *http.Request)
HandleMetadata returns share metadata as JSON — used by the embed.js viewer to decide whether to show the PIN input or jump straight to download.