Help us shape the future of Go! Take the 2026 Go Developer Survey

staging

package
v0.56.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package staging is filex's own upload staging area: the bytes of an upload land here first, on local disk, and are transferred to the storage driver afterwards by a background op.

Why this exists: before it, the only chunked upload path required storage.MultipartUploader — implemented by the S3 driver alone — and on that path the browser PUT its parts straight to S3, so filex never saw the bytes. Every other write was one synchronous request in which the client waited for the backend, which on a slow backend means the progress bar shows the *backend's* speed and a 4 GB upload over a flaky link starts from zero.

Layout

<root>/<id>/manifest.json
<root>/<id>/000001.part
<root>/<id>/000002.part
…

Numbered parts, deliberately — NOT a single append-only <id>.part with a byte offset. A single file plus an offset serves a sequential resumable client and nothing else; an S3-compatible UploadPart API (planned directly on top of this layer) receives parts out of order, numbered, each needing its own ETag. Building the sequential-only version means building the staging layer twice. The numbered store serves both: the sequential protocol is the special case where parts arrive in order, and the per-part md5 makes the S3 composite ETag (md5(concat(part md5s))-N) computable without re-reading the data.

The offset contract

Offset is the total size of the CONTIGUOUS RUN OF PARTS FROM PART 1. It is the resume point and it is authoritative: a client that lost its state asks for the manifest and continues from there. Parts written out of order beyond a hole are kept but do not move the offset until the hole is filled.

Durability

A part is written to a temporary file, fsynced, and only then renamed into place and recorded in the manifest — so a connection that dies mid-chunk leaves the offset exactly where it was, which is the whole point of resume. The manifest is rewritten the same way (temp + rename), so it is never observed half-written.

Index

Constants

View Source
const DiskHeadroomFactor = 1.2

DiskHeadroomFactor is the multiple of the declared size that must be free on the staging filesystem before an upload is accepted. 1.2 leaves room for the manifest, for filesystem overhead, and for the fact that other uploads are landing at the same time.

View Source
const ManifestName = "manifest.json"

ManifestName is the per-upload metadata file inside the staging directory.

View Source
const ManifestVersion = 1

ManifestVersion is bumped when the on-disk shape changes incompatibly.

View Source
const MaxParts = 10000

MaxParts mirrors S3's own multipart ceiling. Keeping the staging store inside the same limit means a staged upload can always be replayed as an S3 multipart upload without re-chunking for count reasons.

Variables

View Source
var (
	ErrNotFound     = errors.New("staging: upload not found")
	ErrBadID        = errors.New("staging: invalid upload id")
	ErrBadPart      = errors.New("staging: invalid part")
	ErrShortPart    = errors.New("staging: short part body")
	ErrIncomplete   = errors.New("staging: upload incomplete")
	ErrNoDiskSpace  = errors.New("staging: not enough free disk space")
	ErrTooManyParts = errors.New("staging: too many parts")
)

Errors returned by the area. Callers map these onto status codes.

Functions

func ValidID

func ValidID(id string) bool

ValidID reports whether id is safe to use as a directory name.

Types

type Area

type Area struct {

	// FreeBytes probes free space on the staging filesystem. Swapped in tests
	// to exercise the disk guard without filling a disk.
	FreeBytes func(dir string) (uint64, error)
	// contains filtered or unexported fields
}

Area is a staging root plus the per-upload write locks.

Reads (manifest, assembled body) take no lock: every file is replaced by rename, so a reader either sees the old file or the new one, never a torn one.

func New

func New(dir string) *Area

New returns an Area rooted at dir. An empty dir yields a disabled area — Enabled() reports false and callers must refuse staged uploads rather than silently writing somewhere unexpected.

func (*Area) Create

func (a *Area) Create(id string, totalSize, chunkSize int64, hash string) (*Manifest, error)

Create makes the staging directory and writes the initial manifest.

func (*Area) CreateVariable

func (a *Area) CreateVariable(id string) (*Manifest, error)

CreateVariable starts a staging directory whose parts may be any size — the shape S3 multipart needs. See Manifest.Variable for why both modes live in one area.

func (*Area) Dir

func (a *Area) Dir(id string) string

Dir returns the staging directory for id ("" when the id is unsafe).

func (*Area) Enabled

func (a *Area) Enabled() bool

Enabled reports whether a staging root is configured.

func (*Area) EnsureFree

func (a *Area) EnsureFree(size int64) error

EnsureFree is the disk guard: it refuses when the staging filesystem has less than size*DiskHeadroomFactor available. A probe that cannot answer (unknown platform, permission) does NOT refuse — a guard that blocks all uploads because it cannot measure is worse than no guard.

func (*Area) Idle

func (a *Area) Idle(id string, now time.Time) (time.Duration, error)

Idle reports how long ago the upload last saw activity, measured from the manifest's mtime (rewritten on every accepted part).

func (*Area) List

func (a *Area) List() ([]string, error)

List returns the ids of every staging directory currently on disk.

func (*Area) Manifest

func (a *Area) Manifest(id string) (*Manifest, error)

Manifest reads the manifest for id.

func (*Area) Open

func (a *Area) Open(id string) (*Reader, error)

Open returns a reader over the assembled upload: every part concatenated in part-number order. The reader is seekable, so a driver (or the S3 SDK) can measure and replay the body, and so chunk 5 can serve a byte range out of staging while the transfer runs.

Refuses an incomplete upload — assembling over a hole would hand the driver a silently truncated file.

func (*Area) Remove

func (a *Area) Remove(id string) error

Remove deletes the whole staging directory for id.

func (*Area) Root

func (a *Area) Root() string

Root returns the staging root directory.

func (*Area) Sweep

func (a *Area) Sweep(ttl time.Duration, now time.Time, keep func(id string) bool) ([]string, error)

Sweep removes staging directories idle for longer than ttl. keep, when non-nil, is consulted first: returning true protects an id (the caller's DB says it is still live, e.g. a failed transfer being kept for retry).

Returns the ids it removed so the caller can log them — a sweep nobody can see is how 29 GB of temp files went unnoticed once already.

func (*Area) Usage

func (a *Area) Usage() (uploads int, bytes int64)

Usage reports what is physically in the staging area right now: how many upload directories, and how many bytes of parts they hold.

It measures the DISK rather than trusting a counter, and that is the point. The in-flight/staged-bytes metrics are moved by events (begin, chunk, commit, abort) so they are fresh between sweeps, then reconciled against this on every sweeper pass — so a restart, or a crash that left parts behind, cannot leave the dashboard lying about how full the staging filesystem is.

func (*Area) WritePart

func (a *Area) WritePart(id string, n int, r io.Reader, want int64) (*Manifest, error)

WritePart stores part n from r.

want is the exact number of bytes the part must contain. A body shorter than want (the client's connection dropped mid-chunk) is DISCARDED and ErrShortPart returned: the offset must never advance over bytes we do not have. A body longer than want is also refused.

Re-writing an existing part is allowed and idempotent — a client that is not sure whether its last chunk landed simply sends it again.

func (*Area) WriteVariablePart

func (a *Area) WriteVariablePart(id string, n int, r io.Reader, max int64) (*Manifest, error)

WriteVariablePart stores part n with whatever length arrives, up to max bytes. Re-writing a part is idempotent — S3 clients retry a part by sending it again, and the last one written wins, exactly as S3 specifies.

⚠ Unlike WritePart there is no expected length to check against, so a truncated body cannot be detected HERE. That is not a gap: S3 puts the integrity check at CompleteMultipartUpload, where the client sends back the ETag of every part it believes it uploaded, and a part whose md5 disagrees is refused there.

type Manifest

type Manifest struct {
	Version   int    `json:"version"`
	ID        string `json:"id"`
	TotalSize int64  `json:"total_size"`
	ChunkSize int64  `json:"chunk_size"`
	Hash      string `json:"hash,omitempty"`
	// Variable switches off the fixed grid.
	//
	// ⚠ The two modes exist because the two protocols disagree. filex's own
	// resumable upload knows the total size up front and cuts it into equal
	// chunks, so a part's exact length is checkable and a short chunk can be
	// refused. S3 multipart knows neither: parts arrive with arbitrary sizes
	// and the total is only known at CompleteMultipartUpload. Forcing S3 onto
	// the fixed grid would mean rejecting perfectly legal uploads; giving S3
	// its own staging area would mean two sweepers, two disk guards and two
	// GCs. So one area, two modes.
	Variable  bool      `json:"variable,omitempty"`
	Parts     []Part    `json:"parts"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Manifest is the on-disk record of what is physically staged. It is the authority for the resume offset; the DB row mirrors it so listings and the sweeper do not have to open every manifest.

func (*Manifest) Complete

func (m *Manifest) Complete() bool

Complete reports whether every part is present and the sizes add up to the declared total.

func (*Manifest) CompositeETag

func (m *Manifest) CompositeETag() string

CompositeETag is the S3-style multipart ETag for the staged parts: md5(concat(raw part md5s))-N. Computed from the manifest alone — the future S3 gateway can answer with it without re-reading a byte.

func (*Manifest) ExpectedPartSize

func (m *Manifest) ExpectedPartSize(n int) int64

ExpectedPartSize is the exact length part n must have: a full chunk, or the remainder for the final part. Returns 0 for an out-of-range number.

func (*Manifest) Offset

func (m *Manifest) Offset() int64

Offset is the resume point: the total size of the contiguous run of parts starting at part 1. A part written past a hole does not count until the hole is filled — which is exactly what a sequential client needs to be told.

func (*Manifest) Part

func (m *Manifest) Part(n int) (Part, bool)

Part returns the recorded part n.

func (*Manifest) PartCount

func (m *Manifest) PartCount() int

PartCount is how many parts a complete upload has under this manifest's grid.

func (*Manifest) Received

func (m *Manifest) Received() int64

Received is the total staged size across ALL parts, holes included. Used for reporting only — never as a resume point.

type Part

type Part struct {
	N    int    `json:"n"`
	Size int64  `json:"size"`
	MD5  string `json:"md5"`
}

Part is one staged chunk: its number (1-based), its byte length and the md5 of its bytes.

type Reader

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

Reader concatenates the part files of a staged upload into one seekable stream. Only one part file is open at a time.

func (*Reader) Close

func (r *Reader) Close() error

Close releases the currently open part file.

func (*Reader) Read

func (r *Reader) Read(p []byte) (int, error)

Read implements io.Reader across the part boundary.

func (*Reader) Seek

func (r *Reader) Seek(offset int64, whence int) (int64, error)

Seek implements io.Seeker over the assembled stream.

func (*Reader) Size

func (r *Reader) Size() int64

Size is the assembled length in bytes.

Jump to

Keyboard shortcuts

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