116 lines
3.8 KiB
Go
116 lines
3.8 KiB
Go
// Package site turns an activated deployment into HTTP responses.
|
|
//
|
|
// Everything here is built around one property: a request must never observe a
|
|
// mixture of two deployments. That is achieved by making the served state an
|
|
// immutable snapshot behind an atomic pointer — a handler loads it once, at the
|
|
// top, and every subsequent decision in that request comes from the value it
|
|
// loaded. Switching versions is one pointer store, so an in-flight request keeps
|
|
// reading the deployment it started on until it finishes.
|
|
package site
|
|
|
|
import (
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/iceBear67/simplepages/internal/cas"
|
|
"github.com/iceBear67/simplepages/internal/store"
|
|
)
|
|
|
|
// FileEntry is what a request path resolves to: enough to serve the bytes and
|
|
// to answer a conditional request, and nothing else.
|
|
type FileEntry struct {
|
|
Digest cas.Digest
|
|
Size int64
|
|
}
|
|
|
|
// Index is the part of a snapshot that costs something to build.
|
|
//
|
|
// It is separate from Deployment so activation can pay for it *before* it
|
|
// changes anything: if the manifest cannot be read or turned into these maps,
|
|
// the failure happens while the old deployment is still the active one.
|
|
type Index struct {
|
|
files map[string]FileEntry
|
|
dirs map[string]struct{}
|
|
totalBytes int64
|
|
}
|
|
|
|
// NewIndex builds the lookup structures for one manifest.
|
|
//
|
|
// Dirs holds every directory prefix, which is what lets Resolve tell "no such
|
|
// path" apart from "that is a directory, redirect to it with a trailing slash"
|
|
// without scanning the file map for prefixes on every miss.
|
|
func NewIndex(files []store.FileRow) *Index {
|
|
idx := &Index{
|
|
files: make(map[string]FileEntry, len(files)),
|
|
dirs: make(map[string]struct{}),
|
|
}
|
|
for _, f := range files {
|
|
idx.files[f.Path] = FileEntry{Digest: f.Digest, Size: f.Size}
|
|
idx.totalBytes += f.Size
|
|
for dir := path.Dir(f.Path); dir != "." && dir != "/"; dir = path.Dir(dir) {
|
|
if _, ok := idx.dirs[dir]; ok {
|
|
// Every shorter prefix was added with this one, so there is
|
|
// nothing left to walk.
|
|
break
|
|
}
|
|
idx.dirs[dir] = struct{}{}
|
|
}
|
|
}
|
|
return idx
|
|
}
|
|
|
|
// Deployment is an immutable snapshot of what a project serves.
|
|
//
|
|
// Nothing mutates one after NewDeployment returns. That is the whole reason a
|
|
// version switch can be a bare pointer store: readers need no synchronisation
|
|
// beyond the atomic load that handed them the snapshot.
|
|
type Deployment struct {
|
|
ID string
|
|
ProjectID int64
|
|
// Dir is the assembled tree, kept for the webroot symlink and for
|
|
// operators. It is empty under assemble_mode=none and is never used to
|
|
// serve a request — content comes from the CAS by digest.
|
|
Dir string
|
|
// CreatedAt is the Last-Modified of every file in the deployment. The CAS
|
|
// file's own mtime would be wrong: blobs are shared across projects and
|
|
// versions, so their mtime means nothing here and would leak when some
|
|
// other project first uploaded the same bytes.
|
|
CreatedAt time.Time
|
|
ActivatedAt time.Time
|
|
FileCount int
|
|
TotalBytes int64
|
|
|
|
idx *Index
|
|
}
|
|
|
|
// NewDeployment pairs a row with an already-built index. It allocates one
|
|
// struct and copies no maps, so activation can call it after the database
|
|
// commit without any risk of failing there.
|
|
func NewDeployment(dep *store.Deployment, idx *Index, dir string) *Deployment {
|
|
d := &Deployment{
|
|
ID: dep.PublicID,
|
|
ProjectID: dep.ProjectID,
|
|
Dir: dir,
|
|
CreatedAt: dep.CreatedAt,
|
|
FileCount: len(idx.files),
|
|
TotalBytes: idx.totalBytes,
|
|
idx: idx,
|
|
}
|
|
if dep.ActivatedAt != nil {
|
|
d.ActivatedAt = *dep.ActivatedAt
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Lookup finds one file by its slash-separated path within the deployment.
|
|
func (d *Deployment) Lookup(rel string) (FileEntry, bool) {
|
|
e, ok := d.idx.files[rel]
|
|
return e, ok
|
|
}
|
|
|
|
// IsDir reports whether rel is a directory prefix of some file.
|
|
func (d *Deployment) IsDir(rel string) bool {
|
|
_, ok := d.idx.dirs[rel]
|
|
return ok
|
|
}
|