From c8a276b248902c41af64e254c04ef8ed8fc954ba Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 28 Mar 2026 21:42:35 +1100 Subject: [PATCH] Have got saving working --- assets/js/controllers/edit_upload.js | 46 +++++++++++- cmds/server.go | 1 + handlers/imageedit.go | 21 ++++++ providers/db/gen/sqlgen/categories.sql.go | 2 +- providers/db/gen/sqlgen/db.go | 2 +- providers/db/gen/sqlgen/models.go | 2 +- providers/db/gen/sqlgen/pages.sql.go | 2 +- .../db/gen/sqlgen/pending_uploads.sql.go | 2 +- providers/db/gen/sqlgen/posts.sql.go | 2 +- providers/db/gen/sqlgen/pubtargets.sql.go | 2 +- providers/db/gen/sqlgen/sites.sql.go | 2 +- providers/db/gen/sqlgen/uploads.sql.go | 26 +++++-- providers/db/gen/sqlgen/users.sql.go | 2 +- providers/db/uploads.go | 13 +++- providers/uploadfiles/provider.go | 5 ++ services/imgedit/service.go | 52 ++++++++++++++ services/imgedit/store.go | 4 ++ services/uploads/manage.go | 72 +++++++++++++++++++ sql/queries/uploads.sql | 5 +- views/uploads/edit.html | 2 +- views/uploads/show.html | 5 +- 21 files changed, 248 insertions(+), 22 deletions(-) diff --git a/assets/js/controllers/edit_upload.js b/assets/js/controllers/edit_upload.js index f575bea..95cbb1e 100644 --- a/assets/js/controllers/edit_upload.js +++ b/assets/js/controllers/edit_upload.js @@ -1,3 +1,4 @@ +import feather from "feather-icons/dist/feather.js"; import Handlebars from "handlebars"; import {Controller} from "@hotwired/stimulus"; @@ -7,12 +8,12 @@ Handlebars.registerHelper("submit_on", function (id, event) { const processorFrame = Handlebars.compile(`
-
+
{{name}} - X + >
{{{props}}}
@@ -78,6 +79,16 @@ export default class UploadEditController extends Controller { await this._removeProcessor(id); } + async saveUpload(ev) { + ev.preventDefault(); + await this._save("replace"); + } + + async saveNewUpload(ev) { + ev.preventDefault(); + await this._save("copy"); + } + async updateProcessor(ev) { ev.preventDefault(); let id = ev.params.id; @@ -108,6 +119,8 @@ export default class UploadEditController extends Controller { }); el.innerHTML += cardOuter; } + + feather.replace(); } async _createSession() { @@ -179,6 +192,33 @@ export default class UploadEditController extends Controller { }) } + async _save(mode) { + if (!this._state || !this._state.session) { + return; + } + + try { + let resp = await fetch(`/sites/${this.siteIdValue}/imageedit/${this._state.session.guid}/save`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ mode }) + }); + + if (!resp.ok) { + console.error("Save failed:", resp.statusText); + return; + } + + let result = await resp.json(); + window.location.href = `/sites/${this.siteIdValue}/uploads/${result.upload_id}`; + } catch (e) { + console.error(e); + } + } + async _doReturningState(fn) { try { this._state = await fn(); diff --git a/cmds/server.go b/cmds/server.go index 7a1445a..28e2ccc 100644 --- a/cmds/server.go +++ b/cmds/server.go @@ -156,6 +156,7 @@ Starting weiro without any arguments will start the server. siteGroup.Patch("/imageedit/:sessionID", ieh.PatchSession) siteGroup.Post("/imageedit/:sessionID/processors", ieh.AddProcessor) siteGroup.Delete("/imageedit/:sessionID/processors/:processorID", ieh.DeleteProcessor) + siteGroup.Post("/imageedit/:sessionID/save", ieh.Save) siteGroup.Get("/imageedit/:sessionID/preview/:versionID", ieh.Preview) siteGroup.Get("/settings", ssh.General) diff --git a/handlers/imageedit.go b/handlers/imageedit.go index ced7f75..27a01b0 100644 --- a/handlers/imageedit.go +++ b/handlers/imageedit.go @@ -114,6 +114,27 @@ func (ieh ImageEditHandlers) DeleteProcessor(c fiber.Ctx) error { }) } +func (ieh ImageEditHandlers) Save(c fiber.Ctx) error { + sessionID := c.Params("sessionID") + if sessionID == "" { + return fiber.ErrBadRequest + } + + var req struct { + Mode string `json:"mode"` + } + if err := c.Bind().JSON(&req); err != nil { + return fiber.ErrBadRequest + } + + result, err := ieh.ImageEditService.Save(c.Context(), sessionID, req.Mode) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(result) +} + func (ieh ImageEditHandlers) PatchSession(c fiber.Ctx) error { var req struct { UpdateProc *imgedit.UpdateProcessorReq `json:"processor"` diff --git a/providers/db/gen/sqlgen/categories.sql.go b/providers/db/gen/sqlgen/categories.sql.go index d5bc40d..95a26e5 100644 --- a/providers/db/gen/sqlgen/categories.sql.go +++ b/providers/db/gen/sqlgen/categories.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: categories.sql package sqlgen diff --git a/providers/db/gen/sqlgen/db.go b/providers/db/gen/sqlgen/db.go index 8eab959..7d9d9e7 100644 --- a/providers/db/gen/sqlgen/db.go +++ b/providers/db/gen/sqlgen/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 package sqlgen diff --git a/providers/db/gen/sqlgen/models.go b/providers/db/gen/sqlgen/models.go index 3df1193..348c1ab 100644 --- a/providers/db/gen/sqlgen/models.go +++ b/providers/db/gen/sqlgen/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 package sqlgen diff --git a/providers/db/gen/sqlgen/pages.sql.go b/providers/db/gen/sqlgen/pages.sql.go index 1d53291..7dd5105 100644 --- a/providers/db/gen/sqlgen/pages.sql.go +++ b/providers/db/gen/sqlgen/pages.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: pages.sql package sqlgen diff --git a/providers/db/gen/sqlgen/pending_uploads.sql.go b/providers/db/gen/sqlgen/pending_uploads.sql.go index 63eeb60..a831bbe 100644 --- a/providers/db/gen/sqlgen/pending_uploads.sql.go +++ b/providers/db/gen/sqlgen/pending_uploads.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: pending_uploads.sql package sqlgen diff --git a/providers/db/gen/sqlgen/posts.sql.go b/providers/db/gen/sqlgen/posts.sql.go index ef3d170..129a49a 100644 --- a/providers/db/gen/sqlgen/posts.sql.go +++ b/providers/db/gen/sqlgen/posts.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: posts.sql package sqlgen diff --git a/providers/db/gen/sqlgen/pubtargets.sql.go b/providers/db/gen/sqlgen/pubtargets.sql.go index 69c09df..cd5cfa6 100644 --- a/providers/db/gen/sqlgen/pubtargets.sql.go +++ b/providers/db/gen/sqlgen/pubtargets.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: pubtargets.sql package sqlgen diff --git a/providers/db/gen/sqlgen/sites.sql.go b/providers/db/gen/sqlgen/sites.sql.go index 80ccbc0..797eaad 100644 --- a/providers/db/gen/sqlgen/sites.sql.go +++ b/providers/db/gen/sqlgen/sites.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: sites.sql package sqlgen diff --git a/providers/db/gen/sqlgen/uploads.sql.go b/providers/db/gen/sqlgen/uploads.sql.go index 189de2d..7ad3828 100644 --- a/providers/db/gen/sqlgen/uploads.sql.go +++ b/providers/db/gen/sqlgen/uploads.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: uploads.sql package sqlgen @@ -18,7 +18,7 @@ func (q *Queries) DeleteUpload(ctx context.Context, id int64) error { return err } -const insertUpload = `-- name: InsertUpload :exec +const insertUpload = `-- name: InsertUpload :one INSERT INTO uploads ( site_id, guid, @@ -43,8 +43,8 @@ type InsertUploadParams struct { CreatedAt int64 } -func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) error { - _, err := q.db.ExecContext(ctx, insertUpload, +func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertUpload, arg.SiteID, arg.Guid, arg.MimeType, @@ -54,7 +54,9 @@ func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) erro arg.Alt, arg.CreatedAt, ) - return err + var id int64 + err := row.Scan(&id) + return id, err } const selectUploadByID = `-- name: SelectUploadByID :one @@ -154,3 +156,17 @@ func (q *Queries) UpdateUpload(ctx context.Context, arg UpdateUploadParams) erro _, err := q.db.ExecContext(ctx, updateUpload, arg.Alt, arg.ID) return err } + +const updateUploadFileSize = `-- name: UpdateUploadFileSize :exec +UPDATE uploads SET file_size = ? WHERE id = ? +` + +type UpdateUploadFileSizeParams struct { + FileSize int64 + ID int64 +} + +func (q *Queries) UpdateUploadFileSize(ctx context.Context, arg UpdateUploadFileSizeParams) error { + _, err := q.db.ExecContext(ctx, updateUploadFileSize, arg.FileSize, arg.ID) + return err +} diff --git a/providers/db/gen/sqlgen/users.sql.go b/providers/db/gen/sqlgen/users.sql.go index 6007589..a70a3bf 100644 --- a/providers/db/gen/sqlgen/users.sql.go +++ b/providers/db/gen/sqlgen/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.30.0 // source: users.sql package sqlgen diff --git a/providers/db/uploads.go b/providers/db/uploads.go index 006b7cc..b3033ab 100644 --- a/providers/db/uploads.go +++ b/providers/db/uploads.go @@ -44,7 +44,7 @@ func (db *Provider) SelectUploadBySiteIDAndSlug(ctx context.Context, siteID int6 func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error { if upload.ID == 0 { - if err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{ + newID, err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{ SiteID: upload.SiteID, Guid: upload.GUID, MimeType: upload.MIMEType, @@ -53,9 +53,11 @@ func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error Slug: upload.Slug, Alt: upload.Alt, CreatedAt: upload.CreatedAt.Unix(), - }); err != nil { + }) + if err != nil { return err } + upload.ID = newID return nil } @@ -65,6 +67,13 @@ func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error }) } +func (db *Provider) UpdateUploadFileSize(ctx context.Context, id int64, fileSize int64) error { + return db.queries.UpdateUploadFileSize(ctx, sqlgen.UpdateUploadFileSizeParams{ + FileSize: fileSize, + ID: id, + }) +} + func (db *Provider) DeleteUpload(ctx context.Context, id int64) error { return db.queries.DeleteUpload(ctx, id) } diff --git a/providers/uploadfiles/provider.go b/providers/uploadfiles/provider.go index 2eb84e4..610a6f9 100644 --- a/providers/uploadfiles/provider.go +++ b/providers/uploadfiles/provider.go @@ -66,6 +66,11 @@ func copyFile(src, dst string) error { return err } +func (p *Provider) ReplaceFile(site models.Site, up models.Upload, srcPath string) error { + fullPath := p.uploadFileName(site, up) + return copyFile(srcPath, fullPath) +} + func (p *Provider) OpenUpload(site models.Site, up models.Upload) (io.ReadCloser, error) { fullPath := p.uploadFileName(site, up) return os.Open(fullPath) diff --git a/services/imgedit/service.go b/services/imgedit/service.go index c53a37e..926633c 100644 --- a/services/imgedit/service.go +++ b/services/imgedit/service.go @@ -175,6 +175,58 @@ func (s *Service) UpdateProcessor(ctx context.Context, sessionID string, req Upd return session, nil } +type SaveResult struct { + UploadID int64 `json:"upload_id"` +} + +func (s *Service) Save(ctx context.Context, sessionID string, mode string) (*SaveResult, error) { + session, err := s.loadAndVerifySession(ctx, sessionID) + if err != nil { + return nil, err + } + + if len(session.Processors) == 0 { + return nil, fmt.Errorf("no processors in session") + } + + lastProc := session.Processors[len(session.Processors)-1] + finalImagePath := fmt.Sprintf("%v/%v/%v.%v", s.scratchDir, session.GUID, lastProc.VersionID, session.ImageExt) + + var mimeType string + switch session.ImageExt { + case "jpg", "jpeg": + mimeType = "image/jpeg" + case "png": + mimeType = "image/png" + } + + var uploadID int64 + switch mode { + case "replace": + upload, err := s.uploadService.ReplaceUploadFile(ctx, session.BaseUploadID, finalImagePath) + if err != nil { + return nil, err + } + uploadID = upload.ID + case "copy": + baseUpload, _, err := s.uploadService.OpenUpload(ctx, session.BaseUploadID) + if err != nil { + return nil, err + } + upload, err := s.uploadService.CreateUploadFromFile(ctx, finalImagePath, baseUpload.Filename, mimeType) + if err != nil { + return nil, err + } + uploadID = upload.ID + default: + return nil, fmt.Errorf("unknown save mode: %v", mode) + } + + s.sessionStore.delete(session.GUID) + + return &SaveResult{UploadID: uploadID}, nil +} + func (s *Service) loadAndVerifySession(ctx context.Context, sessionID string) (*models.ImageEditSession, error) { site, user, err := s.fetchSiteAndUser(ctx) if err != nil { diff --git a/services/imgedit/store.go b/services/imgedit/store.go index 7638dbe..df3403a 100644 --- a/services/imgedit/store.go +++ b/services/imgedit/store.go @@ -42,6 +42,10 @@ func (ss *sessionStore) get(guid string) (*models.ImageEditSession, error) { return &sessionData, nil } +func (ss *sessionStore) delete(guid string) { + os.RemoveAll(filepath.Join(ss.baseDir, guid)) +} + func (ss *sessionStore) getImage(session *models.ImageEditSession, imageFilename string) (string, func() (io.ReadCloser, error), error) { fullPath := filepath.Join(ss.baseDir, session.GUID, imageFilename) if s, err := os.Stat(fullPath); err != nil { diff --git a/services/uploads/manage.go b/services/uploads/manage.go index 32debac..9cb24ea 100644 --- a/services/uploads/manage.go +++ b/services/uploads/manage.go @@ -6,7 +6,10 @@ import ( "html/template" "io" "log" + "os" + "path/filepath" "strings" + "time" "lmika.dev/lmika/weiro/models" ) @@ -67,6 +70,75 @@ func (s *Service) renderCopyTemplate(upload models.Upload) string { return sb.String() } +func (s *Service) ReplaceUploadFile(ctx context.Context, uploadID int64, srcPath string) (models.Upload, error) { + site, _, err := s.fetchSiteAndUser(ctx) + if err != nil { + return models.Upload{}, err + } + + upload, err := s.db.SelectUploadByID(ctx, uploadID) + if err != nil { + return models.Upload{}, err + } else if upload.SiteID != site.ID { + return models.Upload{}, models.NotFoundError + } + + if err := s.up.ReplaceFile(site, upload, srcPath); err != nil { + return models.Upload{}, err + } + + stat, err := os.Stat(srcPath) + if err != nil { + return models.Upload{}, err + } + upload.FileSize = stat.Size() + + if err := s.db.UpdateUploadFileSize(ctx, upload.ID, upload.FileSize); err != nil { + return models.Upload{}, err + } + + return upload, nil +} + +func (s *Service) CreateUploadFromFile(ctx context.Context, srcPath string, filename string, mimeType string) (models.Upload, error) { + site, _, err := s.fetchSiteAndUser(ctx) + if err != nil { + return models.Upload{}, err + } + + stat, err := os.Stat(srcPath) + if err != nil { + return models.Upload{}, err + } + + newUploadGUID := models.NewNanoID() + newTime := time.Now().UTC() + newSlug := filepath.Join( + fmt.Sprintf("%04d", newTime.Year()), + fmt.Sprintf("%02d", newTime.Month()), + newUploadGUID+filepath.Ext(filename), + ) + + newUpload := models.Upload{ + SiteID: site.ID, + GUID: models.NewNanoID(), + FileSize: stat.Size(), + MIMEType: mimeType, + Filename: filename, + CreatedAt: newTime, + Slug: newSlug, + } + if err := s.db.SaveUpload(ctx, &newUpload); err != nil { + return models.Upload{}, err + } + + if err := s.up.AdoptFile(site, newUpload, srcPath); err != nil { + return models.Upload{}, err + } + + return newUpload, nil +} + func (s *Service) ListUploads(ctx context.Context) (res []UploadWithURL, _ error) { site, _, err := s.fetchSiteAndUser(ctx) if err != nil { diff --git a/sql/queries/uploads.sql b/sql/queries/uploads.sql index fc8b82d..f661591 100644 --- a/sql/queries/uploads.sql +++ b/sql/queries/uploads.sql @@ -7,7 +7,7 @@ SELECT * FROM uploads WHERE id = ? LIMIT 1; -- name: SelectUploadBySiteIDAndSlug :one SELECT * FROM uploads WHERE site_id = ? AND slug = ? LIMIT 1; --- name: InsertUpload :exec +-- name: InsertUpload :one INSERT INTO uploads ( site_id, guid, @@ -23,5 +23,8 @@ RETURNING id; -- name: UpdateUpload :exec UPDATE uploads SET alt = ? WHERE id = ?; +-- name: UpdateUploadFileSize :exec +UPDATE uploads SET file_size = ? WHERE id = ?; + -- name: DeleteUpload :exec DELETE FROM uploads WHERE id = ?; \ No newline at end of file diff --git a/views/uploads/edit.html b/views/uploads/edit.html index 21b41bb..a7b27ab 100644 --- a/views/uploads/edit.html +++ b/views/uploads/edit.html @@ -24,7 +24,7 @@
-
+
diff --git a/views/uploads/show.html b/views/uploads/show.html index 087c10f..7b42a38 100644 --- a/views/uploads/show.html +++ b/views/uploads/show.html @@ -5,7 +5,10 @@ data-show-upload-site-id-value="{{ .upload.Upload.SiteID }}" data-show-upload-upload-id-value="{{ .upload.Upload.ID }}"> - + + Edit + +