Files
pages/internal/deploy/sites.go
T
2026-08-15 07:13:00 +00:00

128 lines
4.2 KiB
Go

package deploy
import (
"context"
"errors"
"time"
"github.com/iceBear67/simplepages/internal/site"
"github.com/iceBear67/simplepages/internal/store"
)
// index reads a deployment's manifest and turns it into the lookup structures
// the serving path needs. This is the expensive, failure-prone half of building
// a snapshot, which is why it is separable: an activation runs it before it
// changes anything.
func (s *Service) index(ctx context.Context, dep *store.Deployment) (*site.Index, error) {
files, err := s.DB.DeploymentFiles(ctx, dep.ID)
if err != nil {
return nil, err
}
return site.NewIndex(files), nil
}
// deploymentDir is where a deployment's tree was assembled, or "" under
// assemble_mode=none, where nothing was.
func (s *Service) deploymentDir(p *store.Project, dep *store.Deployment) string {
if s.Dir == "" {
return ""
}
return DeploymentDir(s.Dir, p.ID, dep.PublicID)
}
// LoadSites builds the registry from the database and makes the webroot agree
// with it. It runs once, before the listeners start.
//
// Without this a restart would serve 404 for every project until each one was
// deployed again. It also has to complete before the registry can stand in as
// the API's project resolver: a half-built registry would report a caller's own
// project as one that does not exist.
func (s *Service) LoadSites(ctx context.Context) error {
projects, err := s.DB.AllProjects(ctx)
if err != nil {
return err
}
entries := make([]*site.Project, 0, len(projects))
var active int
for _, p := range projects {
sp := site.NewProject(p)
entries = append(entries, sp)
dep, err := s.DB.ActiveDeployment(ctx, p.ID)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
// A project that has never been deployed to. It exists, it
// resolves, and it answers 503 until something is activated.
continue
}
return err
}
idx, err := s.index(ctx, dep)
if err != nil {
return err
}
sp.Activate(site.NewDeployment(dep, idx, s.deploymentDir(p, dep)))
active++
}
s.Sites.Replace(entries)
s.Log.Info("site registry loaded", "projects", len(entries), "active", active)
// Best effort, like every other webroot operation: the server serves its own
// content and a stale symlink is not a reason to refuse to start.
if err := s.Reconcile(); err != nil {
s.Log.Warn("could not fully reconcile the webroot", "err", err)
}
return nil
}
// Reconcile makes $WEBROOT agree with what this process is serving: one symlink
// per project with an active deployment, and nothing of ours left over for
// projects that no longer have one.
//
// The wanted set comes from the registry rather than from the database, because
// the registry is what requests are actually being answered from. The symlinks
// exist so that an external reader — a reverse proxy, a backup job — sees the
// same version this process does, and taking them from anywhere else would let
// the two disagree.
func (s *Service) Reconcile() error {
if s.Webroot == nil {
return nil
}
want := make(map[string]string)
for _, p := range s.Sites.Projects() {
// One load per project, and the value is used for both the name and the
// directory: reading Active() twice could straddle an activation and
// produce a link to a directory the other half of the pair disagrees with.
if d := p.Active(); d != nil && d.Dir != "" {
want[p.Name] = d.Dir
}
}
return s.Webroot.Reconcile(want)
}
// RunReconciler repairs the webroot on a timer until ctx is done.
//
// Activation repoints a symlink itself and logs when it cannot, so this is for
// the cases where nothing was there to notice: a link an operator deleted or
// edited, one whose repoint failed on a full disk, or one left pointing at a
// deployment that has since been collected. Serving does not depend on any of
// it, which is why a failure here is a warning and never stops the loop.
func (s *Service) RunReconciler(ctx context.Context, every time.Duration) {
if s.Webroot == nil || every <= 0 {
return
}
t := time.NewTicker(every)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
if err := s.Reconcile(); err != nil {
s.Log.Warn("could not fully reconcile the webroot", "err", err)
}
}
}
}