From a80a7a4dbaad5c6d371dea159a3f4ac8babcef29 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 18 Apr 2025 15:41:29 -0400 Subject: [PATCH] Implement file upload for replies --- .gitignore | 1 + internal/database/handlers.go | 4 +- web/main.go | 77 +++++++++++++++------ web/views/board.templ | 86 ++++++++++++------------ web/views/thread.templ | 122 +++++++++++++++++++--------------- web/views/threads.templ | 28 ++++---- 6 files changed, 188 insertions(+), 130 deletions(-) diff --git a/.gitignore b/.gitignore index 83a6adb..db7cd78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ internal/database/comfychan.db **/*_templ.go tmp +web/static/img/posts diff --git a/internal/database/handlers.go b/internal/database/handlers.go index 662e078..82b97eb 100644 --- a/internal/database/handlers.go +++ b/internal/database/handlers.go @@ -118,8 +118,8 @@ func PutThread(db *sql.DB, boardSlug string, subject string, body string, mediaP return nil } -func PutPost(db *sql.DB, threadId int, body string) error { - _, err := db.Exec(`INSERT INTO posts (thread_id, body) VALUES (?, ?)`, threadId, body) +func PutPost(db *sql.DB, threadId int, body string, mediaPath string) error { + _, err := db.Exec(`INSERT INTO posts (thread_id, body, media_path) VALUES (?, ?, ?)`, threadId, body, mediaPath) if err != nil { return err } diff --git a/web/main.go b/web/main.go index 73e4978..a378dcf 100644 --- a/web/main.go +++ b/web/main.go @@ -2,9 +2,11 @@ package main import ( "database/sql" + "errors" "fmt" "io" "log" + "mime/multipart" "net/http" "os" "path/filepath" @@ -17,6 +19,8 @@ import ( _ "github.com/mattn/go-sqlite3" ) +const FILE_MEM_LIMIT int64 = 10 << 20 + var dev = true func disableCacheInDevMode(next http.Handler) http.Handler { @@ -29,6 +33,24 @@ func disableCacheInDevMode(next http.Handler) http.Handler { }) } +func savePostFile(file *multipart.File, header *multipart.FileHeader) error { + dstPath := filepath.Join("web/static/img/posts", header.Filename) + + dst, err := os.Create(dstPath) + if err != nil { + log.Printf("os.Create: %v", err) + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, *file); err != nil { + log.Printf("io.Copy: %v", err) + return err + } + + return nil +} + func main() { // ----------------- // SETUP @@ -119,37 +141,27 @@ func main() { r.Post("/{slug}/threads", func(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "slug") + subject := r.FormValue("subject") + body := r.FormValue("body") + // 10 MB memory limit - if err := r.ParseMultipartForm(10 << 20); err != nil { - http.Error(w, "Bad form data", http.StatusBadRequest) - log.Printf("ParseForm: %v", err) + if err := r.ParseMultipartForm(FILE_MEM_LIMIT); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + log.Printf("ParseMultipartForm: %v", err) return } file, header, err := r.FormFile("file") if err != nil { - http.Error(w, "Failed to retrieve file from form", http.StatusBadRequest) + http.Error(w, "Failed to retrive file from form", http.StatusBadRequest) log.Printf("FormFile: %v", err) return } defer file.Close() - subject := r.FormValue("subject") - body := r.FormValue("body") - - dstPath := filepath.Join("web/static/img/posts", header.Filename) - - dst, err := os.Create(dstPath) - if err != nil { - http.Error(w, "Failed to create file", http.StatusInternalServerError) - log.Printf("os.Create: %v", err) - return - } - defer dst.Close() - - if _, err := io.Copy(dst, file); err != nil { + if err := savePostFile(&file, header); err != nil { http.Error(w, "Failed to save file", http.StatusInternalServerError) - log.Printf("io.Copy: %v", err) + log.Printf("savePostFile: %v", err) return } @@ -172,8 +184,33 @@ func main() { } body := r.FormValue("body") + mediaPath := "" - if err := database.PutPost(db, threadId, body); err != nil { + // 10 MB memory limit + if err := r.ParseMultipartForm(FILE_MEM_LIMIT); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + log.Printf("ParseMultipartForm: %v", err) + return + } + + file, header, err := r.FormFile("file") + if err != nil { + if !errors.Is(err, http.ErrMissingFile) { + http.Error(w, "Failed to retrive file from form", http.StatusBadRequest) + log.Printf("FormFile: %v", err) + return + } + } else { + defer file.Close() + if err := savePostFile(&file, header); err != nil { + http.Error(w, "Failed to save file", http.StatusInternalServerError) + log.Printf("savePostFile: %v", err) + return + } + mediaPath = header.Filename + } + + if err := database.PutPost(db, threadId, body, mediaPath); err != nil { http.Error(w, "Failed to create post", http.StatusInternalServerError) log.Printf("PutPost: %v", err) return diff --git a/web/views/board.templ b/web/views/board.templ index eb41154..d2dcf23 100644 --- a/web/views/board.templ +++ b/web/views/board.templ @@ -1,56 +1,60 @@ package views import ( -"fmt" -database "github.com/dominicf2001/comfychan/internal/database" -"github.com/dominicf2001/comfychan/web/views/shared" + "fmt" + "github.com/dominicf2001/comfychan/internal/database" + "github.com/dominicf2001/comfychan/web/views/shared" ) templ NewThreadForm(board database.Board) { - + then trigger refreshThreads on body" + > + + + + + + + + + + + + + + + +
Subject
Comment
File
+ + } templ BoardHeader(board database.Board) { -
- -

- { fmt.Sprintf("/%s/ - %s", board.Slug, board.Name) } -

-

{ board.Tag }

-
+
+ +

+ { fmt.Sprintf("/%s/ - %s", board.Slug, board.Name) } +

+

{ board.Tag }

+
} templ Board(board database.Board) { -@shared.Layout() { -@BoardHeader(board) -
- - @NewThreadForm(board) -
-
-} + @shared.Layout() { + @BoardHeader(board) +
+ + @NewThreadForm(board) +
+
+ } } diff --git a/web/views/thread.templ b/web/views/thread.templ index e4fc2c6..e4784ce 100644 --- a/web/views/thread.templ +++ b/web/views/thread.templ @@ -1,65 +1,81 @@ package views import ( -"fmt" -"github.com/dominicf2001/comfychan/internal/database" -"github.com/dominicf2001/comfychan/web/views/shared" -"strconv" + "fmt" + "github.com/dominicf2001/comfychan/internal/database" + "github.com/dominicf2001/comfychan/web/views/shared" + "strconv" ) templ ThreadPosts(posts []database.Post) { -for _, post := range (posts) { -
-
- - - { post.CreatedAt.Format("15:04") } - No. { strconv.Itoa(post.Id) } -
-
-

{ post.Body }

-
-
-
-} + for _, post := range (posts) { +
+ if post.MediaPath != "" { +
+ +
+ } +
+ + + { post.CreatedAt.Format("15:04") } + No. { strconv.Itoa(post.Id) } +
+

{ post.Body }

+
+
+ } } templ Thread(board database.Board, thread database.Thread, posts []database.Post) { -@shared.Layout() { -@BoardHeader(board) -
-
+ - - - - - - - -
comment
- -
-
-
-
- if posts[0].MediaPath != "" { -
- + then trigger refreshPosts on body" + > + + + + + + + + + + + +
comment
File
+ +
- } -
-

{ thread.Subject } -

- - - { posts[0].CreatedAt.Format("15:04") } - No. { strconv.Itoa(posts[0].Id) } -
-

{ posts[0].Body }

-
-
-
-} +
+
+ if posts[0].MediaPath != "" { +
+ +
+ } +
+

{ thread.Subject } -

+ + + { posts[0].CreatedAt.Format("15:04") } + No. { strconv.Itoa(posts[0].Id) } +
+

{ posts[0].Body }

+
+
+
+ } } diff --git a/web/views/threads.templ b/web/views/threads.templ index a1f35e4..c9aa906 100644 --- a/web/views/threads.templ +++ b/web/views/threads.templ @@ -3,22 +3,22 @@ package views import "fmt" type CatalogThreadPreview struct { -Subject string -Body string -ThreadURL string -MediaPath string + Subject string + Body string + ThreadURL string + MediaPath string } templ ThreadsCatalog(previews []CatalogThreadPreview) { -
- for _, preview := range previews { -
- - - -

{ preview.Subject }

-

{ preview.Body }

+
+ for _, preview := range previews { +
+ + + +

{ preview.Subject }

+

{ preview.Body }

+
+ }
- } -
}