Use new upload style

This commit is contained in:
akp 2024-11-10 20:08:40 +00:00
parent 976aae76ac
commit 818adaff4a
No known key found for this signature in database
GPG key ID: CF8D58F3DEB20755
10 changed files with 170 additions and 69 deletions

2
go.mod
View file

@ -10,3 +10,5 @@ require (
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
gopkg.in/yaml.v3 v3.0.1
)
require github.com/google/uuid v1.6.0 // indirect

2
go.sum
View file

@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=

View file

@ -0,0 +1,18 @@
package core
import (
"github.com/codemicro/palmatum/palmatum/internal/config"
"github.com/jmoiron/sqlx"
)
type Core struct {
Config *config.Config
Database *sqlx.DB
}
func New(c *config.Config, db *sqlx.DB) *Core {
return &Core{
Config: c,
Database: db,
}
}

View file

@ -0,0 +1,41 @@
package core
import (
"errors"
"fmt"
"github.com/google/uuid"
"io"
"os"
"path"
)
func (c *Core) IngestSiteArchive(archive io.Reader) (string, error) {
var (
key uuid.UUID
fname string
destinationFile *os.File
)
for destinationFile == nil { // take note of the os.O_EXCL flag here - if the file already exists, os.OpenFile will
// error and cause this loop to be re-run, effectively working as an atomic
// check-and-create-if-not-exists-else-try-a-different-name step
key = uuid.New()
fname = fmt.Sprintf("%s.zip", key)
f, err := os.OpenFile(path.Join(c.Config.Platform.SitesDirectory, fname), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if errors.Is(err, os.ErrExist) {
continue
}
return "", fmt.Errorf("open destination file: %w", err)
}
destinationFile = f
}
defer destinationFile.Close()
if _, err := io.Copy(destinationFile, archive); err != nil {
return "", fmt.Errorf("copy archive to destination file %s: %w", fname, err)
}
return fname, nil
}

View file

@ -0,0 +1,28 @@
package core
import (
"errors"
"fmt"
"github.com/codemicro/palmatum/palmatum/internal/database"
"github.com/mattn/go-sqlite3"
)
func (c *Core) UpsertSite(sm *database.SiteModel) error {
tx, err := c.Database.Beginx()
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
}
defer tx.Rollback()
if err := database.InsertSite(tx, sm); err != nil {
if errors.Is(err, sqlite3.ErrConstraint) {
if err = database.UpdateSite(tx, sm); err != nil {
return fmt.Errorf("update site: %w", err)
}
} else {
return fmt.Errorf("insert site: %w", err)
}
}
return nil
}

View file

@ -35,7 +35,6 @@ func New(fname string) (*sqlx.DB, error) {
case 0:
_, err = db.Exec(`CREATE TABLE sites(
"slug" varchar not null primary key,
"name" varchar not null,
"content_path" varchar not null
)`)
if err != nil {
@ -45,7 +44,7 @@ func New(fname string) (*sqlx.DB, error) {
_, err = db.Exec(`CREATE TABLE routes(
"site" varchar not null,
"domain" varchar not null,
"path" varchar,
"path" varchar default '',
foreign key (site) references sites(slug)
)`)
if err != nil {

View file

@ -1,13 +1,69 @@
package database
import (
"database/sql"
"errors"
"github.com/jmoiron/sqlx"
)
type SiteModel struct {
Slug string `db:"slug"`
Name string `db:"name"`
Slug string `db:"slug"` // primary key
ContentPath string `db:"content_path"`
Routes []*RouteModel `db:"-"`
}
func GetSite(db sqlx.Queryer, slug string) (*SiteModel, error) {
res := new(SiteModel)
if err := db.QueryRowx(`SELECT "slug", "content_path" from sites WHERE "slug" = ?`, slug).Scan(res); err != nil {
return nil, err
}
return res, nil
}
func InsertSite(db sqlx.Ext, site *SiteModel) error {
_, err := sqlx.NamedExec(db, `INSERT INTO sites("slug", "content_path") VALUES(:slug, :content_path)`, site)
return err
}
func UpdateSite(db sqlx.Ext, site *SiteModel) error {
_, err := sqlx.NamedExec(db, `UPDATE sites SET content_path = :content_path WHERE slug = :slug`, site)
return err
}
func GetSites(db sqlx.Queryer) ([]*SiteModel, error) {
var res []*SiteModel
if err := sqlx.Select(db, &res, "SELECT slug, content_path FROM sites"); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
return res, nil
}
func GetSitesWithRoutes(db sqlx.Queryer) ([]*SiteModel, error) {
sites, err := GetSites(db)
if err != nil {
return nil, err
}
smap := make(map[string]*SiteModel)
for _, v := range sites {
smap[v.Slug] = v
}
var routes []*RouteModel
if err := sqlx.Select(db, &routes, "SELECT site, domain, path FROM routes"); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
for _, r := range routes {
smap[r.Site].Routes = append(smap[r.Site].Routes, r)
}
return sites, nil
}
type RouteModel struct {
SiteSlug string `db:"site"`
Domain string `db:"domain"`
Path string `db:"path"`
Site string `db:"site"`
Domain string `db:"domain"`
Path string `db:"path"`
}

View file

@ -2,14 +2,16 @@ package httpsrv
import (
"github.com/codemicro/palmatum/palmatum/internal/config"
"github.com/codemicro/palmatum/palmatum/internal/core"
"github.com/julienschmidt/httprouter"
"net/http"
"strings"
)
func New(conf *config.Config) (http.Handler, error) {
func New(conf *config.Config, c *core.Core) (http.Handler, error) {
r := &routes{
config: conf,
core: c,
}
router := httprouter.New()
@ -23,6 +25,7 @@ func New(conf *config.Config) (http.Handler, error) {
type routes struct {
config *config.Config
core *core.Core
}
func BadRequestResponse(w http.ResponseWriter, message ...string) error {

View file

@ -1,13 +1,12 @@
package httpsrv
import (
"archive/zip"
_ "embed"
"errors"
"fmt"
"github.com/codemicro/palmatum/palmatum/internal/database"
"github.com/julienschmidt/httprouter"
"html/template"
"io"
"net/http"
"os"
"path"
@ -73,72 +72,23 @@ func (ro *routes) uploadSite(w http.ResponseWriter, r *http.Request, _ httproute
}
panic(fmt.Errorf("loading archive request parameter: %w", err))
}
defer formFile.Close()
if formFileHeader.Size > 1000*1000*int64(ro.config.Platform.MaxUploadSizeMegabytes) {
_ = BadRequestResponse(w, fmt.Sprintf("archive too large (maximum size %dMB)", ro.config.Platform.MaxUploadSizeMegabytes))
return
}
zipfileReader, err := zip.NewReader(formFile, formFileHeader.Size)
contentPath, err := ro.core.IngestSiteArchive(formFile)
if err != nil {
panic(fmt.Errorf("creating zip file reader: %w", err))
panic(fmt.Errorf("ingesting site archive: %w", err))
}
// We extract the zip file to a temporary location first as to not end up with pages being served with a mix of old
// and new content while zip file extraction is taking place.
// Create temporary directory
tempDir, err := os.MkdirTemp(ro.config.Platform.SitesDirectory, "")
if err != nil {
panic(fmt.Errorf("creating temporary directory: %w", err))
}
defer os.RemoveAll(tempDir) // by the time this runs we should have copied the directory to its final place but this
// Extract ZIP file contents
for _, file := range zipfileReader.File {
if file.Name[len(file.Name)-1] == '/' {
continue
}
newPath := path.Join(tempDir, file.Name)
dir := path.Dir(newPath)
if _, err := os.Stat(dir); err != nil && errors.Is(err, os.ErrNotExist) {
if err := os.MkdirAll(dir, 0777); err != nil {
panic(fmt.Errorf("creating directory %s during archive unpacking: %w", dir, err))
}
}
fh, err := os.OpenFile(newPath, os.O_WRONLY|os.O_CREATE, 0777)
if err != nil {
panic(fmt.Errorf("creating file %s during archive unpacking: %w", newPath, err))
}
zfh, err := file.Open()
if err != nil {
panic(fmt.Errorf("opening archive file %s during unpacking: %w", file.Name, err))
}
if _, err := io.Copy(fh, zfh); err != nil {
panic(fmt.Errorf("copying file data to %s during archive unpacking: %w", newPath, err))
}
if err := fh.Close(); err != nil {
panic(fmt.Errorf("closing %s during archive unpacking: %w", newPath, err))
}
if err := zfh.Close(); err != nil {
panic(fmt.Errorf("closing zip file %s during archive unpacking: %w", file.Name, err))
}
}
// Delete old directory (if applicable)
permPath := path.Join(ro.config.Platform.SitesDirectory, siteName)
_ = os.RemoveAll(permPath)
// Move temporary directory to new directory
if err := os.Rename(tempDir, permPath); err != nil {
panic(fmt.Errorf("renaming temporary directory: %w", err))
if err := ro.core.UpsertSite(&database.SiteModel{
Slug: siteName,
ContentPath: contentPath,
}); err != nil {
panic(fmt.Errorf("updating site in database: %w", err))
}
if IsBrowser(r) {

View file

@ -3,6 +3,7 @@ package main
import (
"fmt"
"github.com/codemicro/palmatum/palmatum/internal/config"
"github.com/codemicro/palmatum/palmatum/internal/core"
"github.com/codemicro/palmatum/palmatum/internal/database"
"github.com/codemicro/palmatum/palmatum/internal/httpsrv"
"golang.org/x/exp/slog"
@ -12,7 +13,8 @@ import (
func main() {
if err := run(); err != nil {
return
slog.Error("unhandled error", "error", err)
os.Exit(1)
}
}
@ -30,7 +32,7 @@ func run() error {
_ = os.MkdirAll(conf.Platform.SitesDirectory, 0777)
handler, err := httpsrv.New(conf)
handler, err := httpsrv.New(conf, core.New(conf, db))
if err != nil {
return fmt.Errorf("creating HTTP handler: %w", err)
}