pack

package
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package pack turns an Astro project directory into the archive the CLI uploads.

WHAT EXISTS TODAY IS THE WALK, THE LIMITS AND THE ARCHIVE. The walk produces one canonical, ordered file list — the forced exclusions applied first and absolutely, the project's own ignore rules after them with their full semantics — plus the links it skipped and what it has to say about the names it found. The limits read that list: how many files there are, how big the largest is, how big they are together, and, once the archive exists, how big it turned out. The archive turns the list into a deterministic gzipped tar: every header field normalised, the compression level pinned, files only, and a digest computed as the bytes are written. This package describes what it does rather than what it will do, because a package comment read by a stranger is a claim like any other.

THREE OF THE FOUR LIMITS ARE ANSWERED BEFORE ANYTHING IS PACKED and the fourth cannot be, which is the whole of why the limits are not one function. Compressing a project to discover it was never going to be allowed spends the user's time on an answer the file list already held; but the archive adds bytes of its own, so a tree inside every source limit can still pack to something too large, and that refusal has nowhere to live but after the pack. Prepare is where the order between them is kept.

ONE WALK, THREE READERS. The limits, the scan for hard-coded development URLs, and the archive all read the same list, so the list has to be canonical: sorted byte-wise on the slash-separated relative path, identical across two runs, across two machines, and independent of the order a filesystem hands its directories back in.

IT IS NOT ONE OF THE PRE-FLIGHT CHECKS, and it is not run by their engine. The engine hands a check a filesystem seam that cannot express directory enumeration, and widening it would add a method for one caller that no check has any use for. What this package owes instead is the same SHAPE the engine returns — findings, and a manifest row for each question it claims — so the two producers meet in the leaf package that holds the result model, which is a leaf precisely so that this walk and the checks reading its output cannot import each other.

Index

Constants

This section is empty.

Variables

View Source
var DefaultNameScope = NameScope{Public: "public", Source: "src", Pages: "src/pages"}

DefaultNameScope is Astro's own layout, for a project whose config moves neither folder.

Functions

func Limits

func Limits(files []File) check.Results

Limits measures the three source limits over a walked file list and reports all four limit rows.

IT REPORTS THE PACKED ROW TOO, as a decline, and that is what makes it usable for the report a person sees before anything is packed. The combined report has to carry a row for every declared check; the packed size is a fact about an artefact that does not exist yet, so the honest row says the check did not answer rather than answering with a zero.

The decline is BY DESIGN rather than environmental, which decides what it costs: nothing outside the check went wrong, nothing about it is the user's to fix, and a surface that stopped to ask somebody about it would be asking them to decide about the passage of time.

Types

type Archive

type Archive struct {
	// Path is the temporary file holding the archive, created with mode
	// 0600 because it contains the user's own source, which may be
	// private.
	Path string

	// Size is the archive's length in bytes, read back from the file
	// rather than counted on the way past — the question a caller is
	// about to ask an upload about is what is on disk.
	Size int64

	// SHA256 is the hex digest of the archive's bytes, computed DURING
	// the write rather than by reading the file back. A second pass
	// would be a second answer to the same question, and the two could
	// disagree if anything touched the file in between.
	SHA256 string

	// Entries is how many files the archive holds. Directories are not
	// emitted, so this is exactly the length of the list that went in.
	Entries int
}

Archive is the packed project: where the bytes are, how many there are, what they hash to, and how many entries went in.

THE CALLER OWNS THE FILE from the moment this is returned, and Remove is how it gives it back. A failed pack leaves nothing behind and returns the zero value.

func Pack

func Pack(fsys FS, root, dir string, files []File) (Archive, error)

Pack writes files into one deterministic gzipped tar and returns it.

THE FILE LIST IS THE WALK'S, PASSED IN RATHER THAN RE-DISCOVERED. The walk is the single canonical scan this package promises, and a packer that walked again could disagree with the list the user was shown and consented to — which is the one difference nobody would think to look for.

REGULAR FILES AND NOTHING ELSE GO IN. No directory entries, because extraction creates parents and an empty directory carries nothing a site build reads; and no link entries of any kind, because a link entry pointing outside the extraction root is precisely the payload the service's extractor is hardened against, and a client that routinely emitted them would make every real attack look like ordinary traffic.

THIS FUNCTION REFUSES A NON-REGULAR ENTRY RATHER THAN TRUSTING ITS CALLER, and the refusal is why the paragraph above is a property instead of a hope. It previously said the walk never puts a link in the list "and this loop could not emit one if it did" — which was true of the ENTRY and false of the harm. A symlink handed to it was FOLLOWED, because the file is reached through Open, and its target's bytes went into the archive under a regular-file entry: content from outside the project, in the upload, with no link entry anywhere. The sentence was literally true and materially wrong, and the fix is the enforcement rather than a better sentence.

A FIFO is the second reason, and it is not about disclosure: opening one blocks until somebody writes, so a packer that trusted its caller could hang a deploy on a file the user forgot was there.

THE ORDER IS THIS FUNCTION'S OWN, not its caller's. The walk sorts, but a packer that wrote whatever sequence it was handed made determinism a property of the PIPELINE rather than of the packer — the same file set in two orders produced two digests. Sorting here costs a copy and makes the guarantee belong to the function that promises it. The caller's slice is never reordered.

dir is where the temporary archive is created; empty means the system's temporary directory, which is what the CLI passes. It is a parameter because the archive is a file on a real volume: a caller with a reason to choose the volume can, and this package's own suite uses a directory it owns so that "nothing was left behind" is a question it can actually answer.

NOTHING THIS PROGRAM CAN REMOVE SURVIVES A FAILURE. Every error path removes the file before returning, so a caller that got an error has nothing to clean up and no path to clean it up with.

The qualifier is the honest form and the sentence used to lack it: the unlink is best-effort, and an unlink that itself fails leaves the file behind. Reporting that failure was considered and rejected — it replaces the error the caller can act on with one it cannot, and there is no path handed back to retry with. What the sentence may promise is what this code controls.

func (Archive) Remove

func (a Archive) Remove() error

Remove deletes the temporary archive. It is safe to call on a zero-valued Archive, so a caller can defer it beside the error check rather than after it.

type FS

type FS interface {
	// ReadDir lists a directory's entries. The entries describe
	// themselves — a symlink reports as a symlink rather than as
	// whatever it points at — which is what lets the walk decline to
	// follow one without resolving it first.
	ReadDir(name string) ([]fs.DirEntry, error)

	// Open opens a file's contents for reading. This is the operation a
	// read-counting wrapper counts.
	Open(name string) (io.ReadCloser, error)
}

FS is the narrow filesystem surface the walk needs.

IT DECLARES ITS OWN RATHER THAN BORROWING THE PRE-FLIGHT ENGINE'S, and the reason is a shape mismatch rather than a preference: the engine hands a check a seam of stat-and-open, which cannot express directory enumeration, and widening it would add a method for one caller that no check on the other side has any use for. This package must not import that one in any case — the result model is the leaf both of them share, and it is a leaf precisely so the walk and the checks that read the walk's output do not import each other.

OPEN IS HERE, AND THE WALK NEVER CALLS IT ON A PROJECT FILE. That is deliberate: a seam with no content-reading operation would make "the walk reads no file contents" trivially true and unmeasurable, and the claim is worth measuring. A wrapper that counts Open sees the walk reading ignore files and nothing else, and the same wrapper can be asked to read a walked file so that its zero is known to be a real zero rather than a broken instrument.

type File

type File struct {
	// Path is relative to the project root and SLASH-SEPARATED on every
	// platform. It is built by joining components rather than by asking
	// the standard library for a relative path, which would hand back
	// the host's own separator — and this value is sorted, compared and
	// rendered into a result an agent may act on, so a separator that
	// changed with the machine would be a difference nobody asked for.
	Path string
	Size int64
	Mode fs.FileMode
}

File is one regular file the walk will pack.

type NameScope added in v0.1.7

type NameScope struct {
	// Public is the folder Astro copies into the site unchanged. "."
	// means the project root.
	Public string
	// Source is the folder the build compiles, srcDir.
	Source string
	// Pages is the folder whose files become routes, `pages` inside
	// the source folder.
	Pages string
}

NameScope is where the file-name check reads: the folders whose names reach the built site. Both are project-relative and slash-separated, as the caller resolved them from the project's config.

type OSFileSystem

type OSFileSystem struct{}

OSFileSystem is the production FS: the real filesystem, through the standard library.

func (OSFileSystem) Open

func (OSFileSystem) Open(name string) (io.ReadCloser, error)

Open implements FS.

func (OSFileSystem) ReadDir

func (OSFileSystem) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir implements FS. The standard library returns entries sorted by name; the walk does not rely on that, and one of its rows reverses the order on purpose to prove it.

func (OSFileSystem) Readlink(name string) (string, error)

Readlink reads where a link points, without following it.

type Prepared

type Prepared struct {
	Archive Archive

	// Receipt is the success summary: file count, source total, archive
	// size. It is the receipt for an upload that is about to happen, and
	// it is empty on every path that will not happen.
	Receipt string
}

Prepared is what a run that passed every local limit is holding: the archive, and the one line that says what is about to leave the machine.

A run that was refused gets the zero value, whose Archive has no Path — so "was anything packed" is a question about the value rather than about which branch the caller believed it took.

func Prepare

func Prepare(fsys FS, root, dir string, files []File, limits check.Results) (Prepared, check.Results, error)

Prepare packs a project the source limits have already cleared, then answers the one limit whose subject does not exist until they have: the size of the archive itself. It emits the packed row, and only that row.

Why the measuring moved out

This used to run the three source limits ITSELF and pack only if they came back clean — one function, so a caller could not get the order wrong. That shape cannot serve the deploy sequence, where the login sits between the limits and the pack, and the reason is not merely the distance:

  • the source limits would be measured TWICE, at two call sites, one of them invisible from outside. A guard reachable from two call sites is a guard whose deletion mutation lies — delete the copy in here and nothing goes red, because the earlier measurement already refused the same project;
  • and the two answers could not be combined at all. Both would build manifest rows for the same limit ids, and a report claiming one check twice is a duplicate-coverage refusal.

What replaced it, and what it still guarantees

The verdict arrives as an ARGUMENT. This refuses to pack unless the report it is handed both COVERS the three source limits and carries no finding — so the rule the one-function shape existed to protect is still a property of the type rather than of a caller's memory: nothing is packed that the limits refused, and no receipt is printed above a refusal. A caller has to produce the verdict to get a pack at all.

COVERAGE IS CHECKED AS WELL AS CONTENT, and that is the half an argument-shaped gate needs to be a gate. "Carries no finding" is true of a zero check.Results, so without the coverage question a caller that never measured anything would pack — which is precisely the order-by-memory this is supposed to replace, wearing a parameter.

NOTHING IS LEFT ON DISK BY A REFUSAL. A refused report never creates the archive; a run stopped by the packed size removes the one it made. A caller that got a zero Prepared has nothing to clean up, which is the same promise Pack makes about its own failures.

The error return covers I/O — a file that vanished, a disk that filled — and the two refusals above, which are wiring mistakes rather than anything a user did. A project that is simply too large to send is not an error here: it is a finding, and the caller renders it.

type Tree

type Tree struct {
	Files    []File
	Symlinks []string
	Results  check.Results
}

Tree is the walk's whole answer: what would be packed, what was skipped, and what the walk has to say about the project.

THE THIRD FIELD IS THE SAME SHAPE THE PRE-FLIGHT ENGINE RETURNS. There is more than one producer of findings by design, and the place where two of them meet takes them as they come rather than re-wrapping at the call site.

func Walk

func Walk(fsys FS, root string, names NameScope) (Tree, error)

Walk produces one canonical, ordered file list for a project.

It is the single scan three later steps read — the local limits, the scan for hard-coded development URLs, and the archive itself — so it has to answer the same way twice, on two machines, whatever order the filesystem hands its directories back in.

THE EXCLUSION ORDER IS FIXED AND IT IS THE SECURITY PROPERTY. The forced set is evaluated FIRST and cannot be switched off by anything inside the project; the project's own ignore rules come next, with their full semantics including negation; everything else is included. A negation that could re-enable a forced exclude would let a file in the repository decide that a secrets file gets published.

Nothing here follows a symbolic link, which is what makes a link pointing at one of its own ancestors harmless, and nothing here reads a file's contents except an ignore file — whose rules are bytes and cannot be had any other way.

names says where the file-name check reads, as the caller resolved it from the project's config; see NameScope.

Jump to

Keyboard shortcuts

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