sharing

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package sharing holds vrok's domain model: what a share is, how long it lives and how many times it may be fetched. It knows nothing about HTTP, tunnels or the terminal, which keeps the rules testable in isolation.

Index

Constants

View Source
const DownloadLimitGrace = 30 * time.Second

DownloadLimitGrace is how long a share that has used its download allowance stays registered after its last transfer goes quiet. A browser playing a video pauses its fetch once it has buffered enough and resumes with a new ranged request later; without a grace period, that resume would find the share gone and the video would stop partway through.

Variables

View Source
var (
	// ErrRevoked means the owner stopped the share explicitly.
	ErrRevoked = errors.New("sharing: share revoked")
	// ErrExpired means the share outlived its TTL.
	ErrExpired = errors.New("sharing: share expired")
	// ErrDownloadLimit means the download allowance is used up.
	ErrDownloadLimit = errors.New("sharing: download limit reached")
	// ErrNotFound means no share matched the supplied token or id.
	ErrNotFound = errors.New("sharing: share not found")
)

Availability failures. The HTTP layer maps all of them to "this share is gone" responses; it never reveals which condition tripped beyond what the visitor already knows.

View Source
var ErrDuplicate = errors.New("sharing: share already registered")

ErrDuplicate is returned when a share id or token is already registered.

Functions

func ParseHTTPTarget

func ParseHTTPTarget(arg string) (string, bool)

ParseHTTPTarget normalises the accepted shorthands for a local HTTP service into an absolute origin URL:

http://localhost:3000  ->  http://localhost:3000
localhost:3000         ->  http://localhost:3000
:3000                  ->  http://127.0.0.1:3000
3000                   ->  http://127.0.0.1:3000

func RemainingDownloads

func RemainingDownloads(s Snapshot) (int, bool)

RemainingDownloads returns how many downloads are left, and whether a limit applies at all.

func TimeLeft

func TimeLeft(s Snapshot, now time.Time) (time.Duration, bool)

TimeLeft returns the duration until the share expires. The second result is false for shares with no TTL.

func Unavailable

func Unavailable(err error) bool

Unavailable reports whether err is one of the terminal availability errors, i.e. whether the correct answer to the visitor is "gone".

Types

type Clock

type Clock interface {
	Now() time.Time
}

Clock abstracts the passage of time so expiry logic can be tested without sleeping.

type Entry

type Entry struct {
	Name string `json:"name"`
	Path string `json:"path"`
	Size int64  `json:"size"`
}

Entry is one named file inside a share. Path is an absolute local path that was validated when the share was created; Name is the only part a visitor ever sees or sends back.

type Factory

type Factory struct {
	// contains filtered or unexported fields
}

Factory assembles shares from a Source plus Options. It is the only place that mints ids and tokens, which keeps the "no sequential identifiers" rule enforceable in one spot.

func NewFactory

func NewFactory(tokens TokenSource, hasher PasswordHasher, clock Clock) *Factory

NewFactory returns a Factory. clock may be nil, in which case the system clock is used.

func (*Factory) Create

func (f *Factory) Create(src Source, opts Options) (*Share, error)

Create builds a share from src and opts.

type Guard

type Guard interface {
	Check(s Snapshot, now time.Time) error
}

Guard decides whether a share may still be served. Keeping each rule behind this interface means new rules (IP allowlists, access windows, quotas) can be added without touching the HTTP handlers that enforce them.

func NotExpired

func NotExpired() Guard

NotExpired rejects shares past their TTL. A zero ExpiresAt never expires.

func NotRevoked

func NotRevoked() Guard

NotRevoked rejects shares the owner has stopped.

func UnderDownloadLimit

func UnderDownloadLimit() Guard

UnderDownloadLimit rejects shares whose download allowance is spent. The authoritative check happens in Share.ClaimDownload; this guard exists so that browsing pages and listings also stop working once the limit is hit.

type GuardFunc

type GuardFunc func(Snapshot, time.Time) error

GuardFunc adapts a plain function to Guard.

func (GuardFunc) Check

func (f GuardFunc) Check(s Snapshot, now time.Time) error

Check implements Guard.

type Guards

type Guards []Guard

Guards runs a set of guards in order and reports the first failure.

func DefaultGuards

func DefaultGuards() Guards

DefaultGuards returns the availability rules every share is subject to.

func (Guards) Check

func (g Guards) Check(s Snapshot, now time.Time) error

Check implements Guard.

type Kind

type Kind uint8

Kind enumerates the things vrok can expose.

const (
	// KindFile is a single local file.
	KindFile Kind = iota
	// KindFiles is a set of local files presented through an index page.
	KindFiles
	// KindDirectory is a local directory tree, browsable in place.
	KindDirectory
	// KindHTTP is a local HTTP service, reverse-proxied as-is.
	KindHTTP
)

func (Kind) String

func (k Kind) String() string

String implements fmt.Stringer.

type Manager

type Manager interface {
	Resolver
	ByID(id string) (*Share, error)
	List() []*Share
	Remove(id string) bool
}

Manager is the management surface used by the CLI commands.

type Options

type Options struct {
	// TTL is how long the share lives. Zero means "until the process exits".
	TTL time.Duration
	// MaxDownloads caps completed transfers; zero means unlimited.
	MaxDownloads int
	// Password is plaintext. The factory hashes it immediately and never
	// copies it into the share, so it exists only for the life of this call.
	Password string
	// Name overrides the display name.
	Name string
}

Options are the per-share settings a user can pass on the command line.

type PasswordHasher

type PasswordHasher interface {
	Hash(password string) (string, error)
}

PasswordHasher converts a plaintext password into a storable digest.

type Reaper

type Reaper struct {

	// OnExpire is called once per expired share, outside the registry lock.
	OnExpire func(*Share)
	// OnEmpty is called when the reaper removes the final share, which is how
	// `vrok share --ttl` ends without user interaction.
	OnEmpty func()
	// contains filtered or unexported fields
}

Reaper periodically drops shares that have expired or exhausted their download allowance.

Expiry is also enforced on every request, so the reaper is not what makes a share safe; it exists to release memory and to let the CLI exit on its own once the last share is gone.

func NewReaper

func NewReaper(reg *Registry, clock Clock, interval time.Duration) *Reaper

NewReaper returns a reaper checking the registry every interval.

func (*Reaper) Run

func (r *Reaper) Run(ctx context.Context)

Run blocks until ctx is cancelled, purging expired shares as it goes.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the in-memory index of live shares. V1 deliberately has no database: shares exist only while the process that created them runs, which is exactly the lifetime guarantee vrok promises its users.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Add

func (r *Registry) Add(s *Share) error

Add registers a share. Both the id and the token must be unused.

func (*Registry) ByID

func (r *Registry) ByID(id string) (*Share, error)

ByID implements Manager.

func (*Registry) ByToken

func (r *Registry) ByToken(token string) (*Share, error)

ByToken implements Resolver.

func (*Registry) Len

func (r *Registry) Len() int

Len returns the number of registered shares.

func (*Registry) List

func (r *Registry) List() []*Share

List returns every registered share, oldest first.

func (*Registry) PurgeExpired

func (r *Registry) PurgeExpired(now time.Time) []*Share

PurgeExpired removes shares that are no longer available at now, returning them so the caller can report what went away.

func (*Registry) Remove

func (r *Registry) Remove(id string) bool

Remove revokes and unregisters the share with the given id. Revoking before unregistering closes the window where an in-flight request still holds a pointer to the share and would otherwise be allowed to continue.

func (*Registry) RemoveAll

func (r *Registry) RemoveAll() []*Share

RemoveAll revokes and unregisters every share, returning what was removed.

type Resolver

type Resolver interface {
	ByToken(token string) (*Share, error)
}

Resolver turns a URL token into a share. The HTTP layer needs nothing more than this, so that is all it depends on.

type Share

type Share struct {
	// contains filtered or unexported fields
}

Share is a live share: a Spec plus mutable access counters.

Every mutable field is guarded by mu. Counters are deliberately not atomics: the download limit has to be read and incremented as one step, otherwise two simultaneous requests could both slip past the final permitted download.

func New

func New(spec Spec) *Share

New returns a live share for spec.

func (*Share) AddBytes

func (s *Share) AddBytes(n int64)

AddBytes accumulates transferred payload bytes for local statistics.

func (*Share) AllowOneMore added in v0.2.0

func (s *Share) AllowOneMore()

AllowOneMore caps the share at one further download, counting from now, which is what makes a running share a one-time link. Downloads already served do not use up the new allowance.

func (*Share) BeginTransfer added in v0.1.2

func (s *Share) BeginTransfer() *Transfer

BeginTransfer records that a file body has started streaming. Every call must be paired with End on the returned Transfer.

func (*Share) ClaimDownload

func (s *Share) ClaimDownload(now time.Time) (int, error)

ClaimDownload reserves one unit of the download allowance and reports the resulting count. It returns ErrDownloadLimit if the allowance is exhausted, so the caller can refuse the transfer before writing a single byte.

Claiming up front (rather than counting on completion) is what makes --downloads a hard limit under concurrency.

func (*Share) Describe

func (s *Share) Describe() string

Describe renders the share's source in a form suitable for the terminal.

func (*Share) ID

func (s *Share) ID() string

ID returns the public handle.

func (*Share) ReleaseDownload

func (s *Share) ReleaseDownload()

ReleaseDownload returns a previously claimed download to the pool. It is called when a transfer fails before delivering anything, so a dropped connection does not silently consume a visitor's allowance.

func (*Share) Revoke

func (s *Share) Revoke()

Revoke permanently disables the share. It is idempotent.

func (*Share) SetExpiresAt added in v0.2.0

func (s *Share) SetExpiresAt(t time.Time)

SetExpiresAt updates when the share stops answering. The zero time means the share lives until the process exits. The URL does not change.

func (*Share) SetMaxDownloads added in v0.2.0

func (s *Share) SetMaxDownloads(n int)

SetMaxDownloads updates the download cap. Zero means unlimited. Setting 1 turns the share into a one-time link without minting a new URL.

func (*Share) SetPasswordHash added in v0.2.0

func (s *Share) SetPasswordHash(hash string)

SetPasswordHash stores a new Argon2id digest. An empty hash removes the password. The plaintext never enters the share.

func (*Share) Snapshot

func (s *Share) Snapshot() Snapshot

Snapshot returns a consistent view of the share.

func (*Share) Spec

func (s *Share) Spec() Spec

Spec returns a copy of the share's description.

func (*Share) Token

func (s *Share) Token() string

Token returns the URL secret.

func (*Share) Touch

func (s *Share) Touch(now time.Time)

Touch records that a visitor interacted with the share.

type Snapshot

type Snapshot struct {
	Spec
	Downloads        int       `json:"downloads"`
	BytesTransferred int64     `json:"bytes_transferred"`
	LastAccess       time.Time `json:"last_access"`
	Revoked          bool      `json:"revoked"`
	// ActiveTransfers counts file bodies being streamed right now. A share
	// that has used its download allowance is kept until this reaches zero,
	// so the last permitted download is never cut off mid-transfer.
	ActiveTransfers int `json:"active_transfers"`
	// Transfers is the progress of each body being streamed, oldest first.
	Transfers []TransferProgress `json:"transfers,omitempty"`
}

Snapshot is a consistent, read-only view of a share at one instant. Guards and the CLI consume snapshots so they cannot mutate live state by accident.

type Source

type Source struct {
	Kind    Kind
	Root    string
	Entries []Entry
	Target  string
	Name    string
}

Source is a validated description of what the user asked to share, before any ids, tokens or expiry are attached to it.

func Classify

func Classify(args []string) (Source, error)

Classify turns raw CLI arguments into a Source.

Filesystem paths win over URL shorthand: a local file called "8080" is shared as a file, not mistaken for a port. Only when nothing exists on disk does vrok try to read the argument as an HTTP target.

type Spec

type Spec struct {
	// ID is the short public handle used for management commands and, with a
	// relay, as the hostname label.
	ID string `json:"id"`
	// Token is the 128-bit secret that appears in the URL path. Possession of
	// the token is what authorises access.
	Token string `json:"token"`
	// Name is what the share is called in listings and page titles.
	Name string `json:"name"`
	// Kind selects which handler serves the share.
	Kind Kind `json:"kind"`
	// Root is the confined directory for KindDirectory.
	Root string `json:"root,omitempty"`
	// Entries lists the files for KindFile and KindFiles.
	Entries []Entry `json:"entries,omitempty"`
	// Target is the upstream origin for KindHTTP, e.g. http://127.0.0.1:3000.
	Target string `json:"target,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	// ExpiresAt is when the share stops answering. The zero value means the
	// share lives as long as the process does.
	ExpiresAt time.Time `json:"expires_at"`
	// MaxDownloads caps completed file transfers; zero means unlimited.
	MaxDownloads int `json:"max_downloads"`
	// PasswordHash is an Argon2id digest, or empty for an unprotected share.
	// The plaintext password is never stored anywhere.
	PasswordHash string `json:"-"`
}

Spec is the description of a share. Identity fields (ID, Token, Kind, Root, Entries, Target) are fixed at creation so the URL never rotates. Lifetime fields (ExpiresAt, MaxDownloads, PasswordHash) may be updated in place under the share lock, which is how a running share grows a password or becomes one-time without minting a new link.

func (Spec) Protected

func (s Spec) Protected() bool

Protected reports whether the share requires a password.

type SystemClock

type SystemClock struct{}

SystemClock reads the wall clock.

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now implements Clock.

type TokenSource

type TokenSource interface {
	NewID() (string, error)
	NewToken() (string, error)
}

TokenSource supplies the random identifiers a share needs. The concrete implementation lives in internal/security; declaring the interface here keeps the domain free of any dependency on a crypto package.

type Transfer added in v0.2.0

type Transfer struct {
	// contains filtered or unexported fields
}

Transfer is one file body being streamed. Bytes are reported as they are written, so the owner sees a multi-gigabyte download progress live rather than only once it ends.

func (*Transfer) Add added in v0.2.0

func (t *Transfer) Add(n int64)

Add counts n payload bytes as delivered, both for this transfer and for the share's running total.

func (*Transfer) End added in v0.2.0

func (t *Transfer) End(now time.Time)

End records that the transfer finished, successfully or not. The end of a long download is the share's most recent activity, so it also counts as an access.

func (*Transfer) SetTotal added in v0.2.0

func (t *Transfer) SetTotal(n int64)

SetTotal records the expected body size once the response headers say it.

type TransferProgress added in v0.2.0

type TransferProgress struct {
	// Sent is the payload bytes delivered so far.
	Sent int64 `json:"sent"`
	// Total is the expected body size, or -1 when it is not known up front,
	// as with a folder streamed as a zip.
	Total int64 `json:"total"`
}

TransferProgress is how far one in-flight transfer has got.

Jump to

Keyboard shortcuts

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