init
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
package adminapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/iceBear67/simplepages/api"
|
||||
"github.com/iceBear67/simplepages/internal/httpx"
|
||||
"github.com/iceBear67/simplepages/internal/store"
|
||||
)
|
||||
|
||||
// createProject handles POST /api/v1/projects.
|
||||
func (s *Server) createProject(w http.ResponseWriter, r *http.Request) error {
|
||||
var req api.CreateProjectRequest
|
||||
if err := httpx.DecodeJSON(w, r, s.maxJSON(), &req); err != nil {
|
||||
return err
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if err := checkProjectName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := store.DefaultProject(name)
|
||||
s.clampDefaults(p)
|
||||
if err := s.applyPatch(p, req.Patch); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.DB.CreateProject(r.Context(), p); err != nil {
|
||||
if errors.Is(err, store.ErrExists) {
|
||||
return api.Errorf(api.CodeProjectExists, "project %q already exists", name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
httpx.LogAttr(r.Context(), "project", name)
|
||||
if s.Hooks.ProjectChanged != nil {
|
||||
s.Hooks.ProjectChanged(r.Context(), p)
|
||||
}
|
||||
w.Header().Set("Location", api.PathProject(name))
|
||||
httpx.WriteJSON(w, http.StatusCreated, s.projectOf(p))
|
||||
return nil
|
||||
}
|
||||
|
||||
// listProjects handles GET /api/v1/projects.
|
||||
func (s *Server) listProjects(w http.ResponseWriter, r *http.Request) error {
|
||||
limit, err := intQuery(r, "limit", 100, 1, 500)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps, next, err := s.DB.ListProjects(r.Context(), limit, r.URL.Query().Get("cursor"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := api.ProjectList{Projects: make([]api.Project, 0, len(ps)), NextCursor: next}
|
||||
for _, p := range ps {
|
||||
out.Projects = append(out.Projects, s.projectOf(p))
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getProject handles GET /api/v1/projects/{name}.
|
||||
func (s *Server) getProject(w http.ResponseWriter, r *http.Request) error {
|
||||
p, err := s.project(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, s.projectOf(p))
|
||||
return nil
|
||||
}
|
||||
|
||||
// patchProject handles PATCH /api/v1/projects/{name}.
|
||||
func (s *Server) patchProject(w http.ResponseWriter, r *http.Request) error {
|
||||
p, err := s.project(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var patch api.ProjectPatch
|
||||
if err := httpx.DecodeJSON(w, r, s.maxJSON(), &patch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply to a copy so a rejected field cannot leave the caller looking at a
|
||||
// partly-updated project in the error response.
|
||||
updated := *p
|
||||
if err := s.applyPatch(&updated, &patch); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.DB.UpdateProject(r.Context(), &updated); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return api.Errorf(api.CodeNotFound, "no such project")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if s.Hooks.ProjectChanged != nil {
|
||||
s.Hooks.ProjectChanged(r.Context(), &updated)
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, s.projectOf(&updated))
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteProject handles DELETE /api/v1/projects/{name}.
|
||||
//
|
||||
// The cascade takes the project's keys, deployments and manifest rows with it;
|
||||
// blob refcounts fall as the manifest rows go, so the content is reclaimed by
|
||||
// the next GC pass rather than synchronously here.
|
||||
func (s *Server) deleteProject(w http.ResponseWriter, r *http.Request) error {
|
||||
if err := httpx.NoBody(r); err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := s.project(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.DB.DeleteProject(r.Context(), p.ID); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return api.Errorf(api.CodeNotFound, "no such project")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// The project's keys went with it. Nothing else invalidates the auth cache,
|
||||
// so without this a deleted project's key would keep working for up to the
|
||||
// cache TTL.
|
||||
s.Auth.V.Invalidate()
|
||||
if s.Hooks.ProjectDeleted != nil {
|
||||
s.Hooks.ProjectDeleted(r.Context(), p)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// project loads the project named by the {name} wildcard.
|
||||
//
|
||||
// The ownership guard already resolved the same name; looking it up again costs
|
||||
// one indexed read on a cold path and keeps the guard free of any obligation to
|
||||
// hand state to the handler.
|
||||
func (s *Server) project(r *http.Request) (*store.Project, error) {
|
||||
name := r.PathValue("name")
|
||||
if err := checkProjectName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := s.DB.ProjectByName(r.Context(), name)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return nil, api.Errorf(api.CodeNotFound, "no such project")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
httpx.LogAttr(r.Context(), "project", p.Name)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// clampDefaults lowers the schema's per-project defaults to the server-wide
|
||||
// ceilings, so a new project on a server with tightened limits does not start
|
||||
// out already over them.
|
||||
func (s *Server) clampDefaults(p *store.Project) {
|
||||
if n := s.Limits.MaxManifestFiles; n > 0 && p.MaxFiles > n {
|
||||
p.MaxFiles = n
|
||||
}
|
||||
if n := s.Limits.MaxFileBytes; n > 0 && p.MaxFileBytes > n {
|
||||
p.MaxFileBytes = n
|
||||
}
|
||||
if p.MaxTotalBytes < p.MaxFileBytes {
|
||||
p.MaxTotalBytes = p.MaxFileBytes
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user