init
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
package adminapi
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/iceBear67/simplepages/api"
|
||||
"github.com/iceBear67/simplepages/internal/store"
|
||||
)
|
||||
|
||||
// projectOf renders a stored project for the wire.
|
||||
func (s *Server) projectOf(p *store.Project) api.Project {
|
||||
out := api.Project{
|
||||
Name: p.Name,
|
||||
DisplayName: p.DisplayName,
|
||||
IndexFile: p.IndexFile,
|
||||
NotFoundFile: p.NotFoundFile,
|
||||
SPAFallback: p.SPAFallback,
|
||||
CacheControl: p.CacheControl,
|
||||
RetentionCount: p.RetentionCount,
|
||||
RetentionGrace: p.RetentionGraceS,
|
||||
MaxFiles: p.MaxFiles,
|
||||
MaxFileBytes: p.MaxFileBytes,
|
||||
MaxTotalBytes: p.MaxTotalBytes,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
if s.BaseURL != "" {
|
||||
out.URL = api.SiteURL(s.BaseURL, p.Name)
|
||||
}
|
||||
out.ActiveDeployment = s.activeOf(p)
|
||||
return out
|
||||
}
|
||||
|
||||
// activeOf summarises what a project is serving right now.
|
||||
//
|
||||
// It reads the registry rather than the database so listing a hundred projects
|
||||
// stays one query. The summary carries what is being served — id, size,
|
||||
// timestamps — and not the row's metadata or error text; those come from
|
||||
// fetching the deployment itself.
|
||||
func (s *Server) activeOf(p *store.Project) *api.Deployment {
|
||||
if s.Sites == nil {
|
||||
return nil
|
||||
}
|
||||
sp, ok := s.Sites.Lookup(p.Name)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
d := sp.Active()
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
out := &api.Deployment{
|
||||
ID: d.ID,
|
||||
Project: p.Name,
|
||||
State: api.StateReady, // only a ready deployment can be active
|
||||
Active: true,
|
||||
FileCount: d.FileCount,
|
||||
TotalBytes: d.TotalBytes,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
if !d.ActivatedAt.IsZero() {
|
||||
t := d.ActivatedAt
|
||||
out.ActivatedAt = &t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// deploymentOf renders a stored deployment for the wire.
|
||||
//
|
||||
// The row id stays behind: the wire only ever names a deployment by its public
|
||||
// id, so nothing a client holds can be walked to a neighbouring row.
|
||||
func deploymentOf(p *store.Project, d *store.Deployment) api.Deployment {
|
||||
return api.Deployment{
|
||||
ID: d.PublicID,
|
||||
Project: p.Name,
|
||||
State: string(d.State),
|
||||
Active: d.Active,
|
||||
FileCount: d.FileCount,
|
||||
TotalBytes: d.TotalBytes,
|
||||
Meta: d.Meta,
|
||||
Error: d.Error,
|
||||
CreatedAt: d.CreatedAt,
|
||||
FinalizedAt: copyTime(d.FinalizedAt),
|
||||
ActivatedAt: copyTime(d.ActivatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// keyOf renders a stored key. It deliberately has no access to the secret: the
|
||||
// store never loads one in a form that could be rendered, only the hash.
|
||||
func keyOf(k *store.APIKey, projectName string) api.Key {
|
||||
return api.Key{
|
||||
ID: k.ID,
|
||||
Scope: string(k.Scope),
|
||||
Project: projectName,
|
||||
Name: k.Name,
|
||||
CreatedAt: k.CreatedAt,
|
||||
ExpiresAt: copyTime(k.ExpiresAt),
|
||||
LastUsed: copyTime(k.LastUsedAt),
|
||||
RevokedAt: copyTime(k.RevokedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// copyTime defensively copies an optional timestamp so a response value cannot
|
||||
// alias a cached store row.
|
||||
func copyTime(t *time.Time) *time.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
v := *t
|
||||
return &v
|
||||
}
|
||||
|
||||
// applyPatch folds a partial update into p, validating as it goes.
|
||||
//
|
||||
// A patch is all-or-nothing: it is applied to a copy by the caller, so a
|
||||
// rejected field leaves the stored project untouched rather than half-updated.
|
||||
func (s *Server) applyPatch(p *store.Project, patch *api.ProjectPatch) error {
|
||||
if patch == nil {
|
||||
return nil
|
||||
}
|
||||
if v := patch.DisplayName; v != nil {
|
||||
if err := checkText("display_name", *v, maxDisplayNameLen); err != nil {
|
||||
return err
|
||||
}
|
||||
p.DisplayName = *v
|
||||
}
|
||||
if v := patch.IndexFile; v != nil {
|
||||
if err := checkSitePath("index_file", *v); err != nil {
|
||||
return err
|
||||
}
|
||||
p.IndexFile = *v
|
||||
}
|
||||
if v := patch.NotFoundFile; v != nil {
|
||||
// The empty string is how a patch clears the custom 404 document; every
|
||||
// other value must name a real relative path.
|
||||
if *v != "" {
|
||||
if err := checkSitePath("not_found_file", *v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.NotFoundFile = *v
|
||||
}
|
||||
if v := patch.SPAFallback; v != nil {
|
||||
p.SPAFallback = *v
|
||||
}
|
||||
if v := patch.CacheControl; v != nil {
|
||||
if err := checkHeaderValue("cache_control", *v); err != nil {
|
||||
return err
|
||||
}
|
||||
p.CacheControl = *v
|
||||
}
|
||||
if v := patch.RetentionCount; v != nil {
|
||||
// At least one: retaining zero deployments would delete the active one.
|
||||
if *v < 1 || *v > 1000 {
|
||||
return api.Errorf(api.CodeBadRequest, "retention_count must be between 1 and 1000")
|
||||
}
|
||||
p.RetentionCount = *v
|
||||
}
|
||||
if v := patch.RetentionGrace; v != nil {
|
||||
if *v < 0 || *v > 30*24*3600 {
|
||||
return api.Errorf(api.CodeBadRequest,
|
||||
"retention_grace_s must be between 0 and %d", 30*24*3600)
|
||||
}
|
||||
p.RetentionGraceS = *v
|
||||
}
|
||||
if v := patch.MaxFiles; v != nil {
|
||||
if err := checkCeiling("max_files", int64(*v), int64(s.Limits.MaxManifestFiles)); err != nil {
|
||||
return err
|
||||
}
|
||||
p.MaxFiles = *v
|
||||
}
|
||||
if v := patch.MaxFileBytes; v != nil {
|
||||
if err := checkCeiling("max_file_bytes", *v, s.Limits.MaxFileBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
p.MaxFileBytes = *v
|
||||
}
|
||||
if v := patch.MaxTotalBytes; v != nil {
|
||||
if *v < 1 {
|
||||
return api.Errorf(api.CodeBadRequest, "max_total_bytes must be positive")
|
||||
}
|
||||
p.MaxTotalBytes = *v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkCeiling enforces that a per-project limit is positive and does not
|
||||
// exceed the server-wide one. A project may lower its own ceiling but never
|
||||
// raise it past what the operator configured.
|
||||
func checkCeiling(field string, v, ceiling int64) error {
|
||||
if v < 1 {
|
||||
return api.Errorf(api.CodeBadRequest, "%s must be positive", field)
|
||||
}
|
||||
if ceiling > 0 && v > ceiling {
|
||||
return api.Errorf(api.CodeBadRequest,
|
||||
"%s must be at most the server limit of %d", field, ceiling)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user