Use new upload style
This commit is contained in:
parent
976aae76ac
commit
818adaff4a
10 changed files with 170 additions and 69 deletions
2
go.mod
2
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
18
palmatum/internal/core/core.go
Normal file
18
palmatum/internal/core/core.go
Normal 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,
|
||||
}
|
||||
}
|
41
palmatum/internal/core/fs.go
Normal file
41
palmatum/internal/core/fs.go
Normal 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
|
||||
}
|
28
palmatum/internal/core/sites.go
Normal file
28
palmatum/internal/core/sites.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue