mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-25 09:36:24 -07:00
Add ability to replace file uploads
This commit is contained in:
parent
51f00a7983
commit
f7ca807144
7 changed files with 405 additions and 266 deletions
|
@ -1,7 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
|
@ -44,34 +47,50 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
Msg("Error getting post information")
|
||||
return
|
||||
}
|
||||
errEv.Int("postID", post.ID)
|
||||
|
||||
if post.Password != passwordMD5 && rank == 0 {
|
||||
serverutil.ServeErrorPage(writer, "Wrong password")
|
||||
return
|
||||
}
|
||||
|
||||
boardID, err := post.GetBoardID()
|
||||
board, err := post.GetBoard()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Unable to get board ID from post")
|
||||
serverutil.ServeErrorPage(writer, "Unable to get board ID from post: "+err.Error())
|
||||
return
|
||||
}
|
||||
errEv.Str("board", board.Dir)
|
||||
upload, err := post.GetUpload()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
serverutil.ServeErrorPage(writer, "Error getting post upload info: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = serverutil.MinifyTemplate(gctemplates.PostEdit, map[string]interface{}{
|
||||
data := map[string]interface{}{
|
||||
"boards": gcsql.AllBoards,
|
||||
"systemCritical": config.GetSystemCriticalConfig(),
|
||||
"siteConfig": config.GetSiteConfig(),
|
||||
"boardID": boardID,
|
||||
"board": board,
|
||||
"boardConfig": config.GetBoardConfig(""),
|
||||
"post": post,
|
||||
"referrer": request.Referer(),
|
||||
}, writer, "text/html"); err != nil {
|
||||
}
|
||||
if upload != nil {
|
||||
data["upload"] = upload
|
||||
}
|
||||
buf := bytes.NewBufferString("")
|
||||
err = serverutil.MinifyTemplate(gctemplates.PostEdit, data, buf, "text/html")
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Error executing edit post template")
|
||||
serverutil.ServeError(writer, "Error executing edit post template: "+err.Error(), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
writer.Write(buf.Bytes())
|
||||
}
|
||||
if doEdit == "1" {
|
||||
if doEdit == "post" || doEdit == "upload" {
|
||||
var password string
|
||||
postid, err := strconv.Atoi(request.FormValue("postid"))
|
||||
if err != nil {
|
||||
|
@ -116,19 +135,72 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
return
|
||||
}
|
||||
|
||||
if err = post.UpdateContents(
|
||||
request.FormValue("editemail"),
|
||||
request.FormValue("editsubject"),
|
||||
posting.FormatMessage(request.FormValue("editmsg"), board.Dir),
|
||||
request.FormValue("editmsg"),
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("postid", post.ID).
|
||||
Msg("Unable to edit post")
|
||||
serverutil.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
if doEdit == "upload" {
|
||||
oldUpload, err := post.GetUpload()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
serverutil.ServeError(writer, err.Error(), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
upload, gotErr := posting.AttachUploadFromRequest(request, writer, post, board)
|
||||
if gotErr {
|
||||
// AttachUploadFromRequest handles error serving/logging
|
||||
return
|
||||
}
|
||||
if upload == nil {
|
||||
serverutil.ServeError(writer, "Missing upload replacement", wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
var filePath, thumbPath, catalogThumbPath string
|
||||
if oldUpload != nil && oldUpload.Filename != "deleted" {
|
||||
filePath = path.Join(documentRoot, board.Dir, "src", oldUpload.Filename)
|
||||
thumbPath = path.Join(documentRoot, board.Dir, "thumb", oldUpload.ThumbnailPath("thumb"))
|
||||
catalogThumbPath = path.Join(documentRoot, board.Dir, "thumb", oldUpload.ThumbnailPath("catalog"))
|
||||
if err = post.UnlinkUploads(false); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
serverutil.ServeError(writer, "Error unlinking old upload from post: "+err.Error(), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
os.Remove(filePath)
|
||||
os.Remove(thumbPath)
|
||||
if post.IsTopPost {
|
||||
os.Remove(catalogThumbPath)
|
||||
}
|
||||
}
|
||||
if err = post.AttachFile(upload); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("newFilename", upload.Filename).
|
||||
Str("newOriginalFilename", upload.OriginalFilename).
|
||||
Send()
|
||||
serverutil.ServeError(writer, "Error attaching new upload: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
filePath = path.Join(documentRoot, board.Dir, "src", upload.Filename)
|
||||
thumbPath = path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("thumb"))
|
||||
catalogThumbPath = path.Join(documentRoot, board.Dir, "thumb", upload.ThumbnailPath("catalog"))
|
||||
os.Remove(filePath)
|
||||
os.Remove(thumbPath)
|
||||
if post.IsTopPost {
|
||||
os.Remove(catalogThumbPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err = post.UpdateContents(
|
||||
request.FormValue("editemail"),
|
||||
request.FormValue("editsubject"),
|
||||
posting.FormatMessage(request.FormValue("editmsg"), board.Dir),
|
||||
request.FormValue("editmsg"),
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("postid", post.ID).
|
||||
Msg("Unable to edit post")
|
||||
serverutil.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = building.BuildBoards(false, boardid); err != nil {
|
||||
|
|
|
@ -176,7 +176,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
if wantsJSON {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit post" && doEdit != "1" && moveBtn != "Move thread" && doMove != "1" {
|
||||
if action == "" && deleteBtn != "Delete" && reportBtn != "Report" && editBtn != "Edit post" && doEdit != "post" && doEdit != "upload" && moveBtn != "Move thread" && doMove != "1" {
|
||||
gcutil.LogAccess(request).Int("status", 400).Msg("received invalid /util request")
|
||||
if wantsJSON {
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
|
@ -227,7 +227,7 @@ func utilHandler(writer http.ResponseWriter, request *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if editBtn != "" || doEdit == "1" {
|
||||
if editBtn != "" || doEdit == "post" || doEdit == "upload" {
|
||||
editPost(checkedPosts, editBtn, doEdit, writer, request)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ func (p Post) UploadPath() string {
|
|||
}
|
||||
|
||||
func GetBuildablePost(id int, boardid int) (*Post, error) {
|
||||
const query = postQueryBase + " AND DBPREFIXposts.id = ?"
|
||||
const query = postQueryBase + " AND DBPREFIXposts.id = ? GROUP BY DBPREFIXposts.id"
|
||||
var post Post
|
||||
var threadID int
|
||||
err := gcsql.QueryRowSQL(query, []interface{}{id}, []interface{}{
|
||||
|
@ -134,6 +134,7 @@ func GetBuildablePostsByIP(ip string, limit int) ([]Post, error) {
|
|||
if limit > 0 {
|
||||
query += " LIMIT " + strconv.Itoa(limit)
|
||||
}
|
||||
query += " GROUP BY DBPREFIXposts.id"
|
||||
rows, err := gcsql.QuerySQL(query, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -159,7 +160,7 @@ func GetBuildablePostsByIP(ip string, limit int) ([]Post, error) {
|
|||
}
|
||||
|
||||
func getBoardTopPosts(boardID int) ([]Post, error) {
|
||||
const query = postQueryBase + " AND is_top_post AND t.board_id = ?"
|
||||
const query = postQueryBase + " AND is_top_post AND t.board_id = ? GROUP BY DBPREFIXposts.id"
|
||||
rows, err := gcsql.QuerySQL(query, boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -185,7 +186,7 @@ func getBoardTopPosts(boardID int) ([]Post, error) {
|
|||
}
|
||||
|
||||
func getThreadPosts(thread *gcsql.Thread) ([]Post, error) {
|
||||
const query = postQueryBase + " AND DBPREFIXposts.thread_id = ?"
|
||||
const query = postQueryBase + " AND DBPREFIXposts.thread_id = ? GROUP BY DBPREFIXposts.id"
|
||||
rows, err := gcsql.QuerySQL(query, thread.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -219,7 +220,8 @@ func GetRecentPosts(boardid int, limit int) ([]Post, error) {
|
|||
args = append(args, boardid)
|
||||
}
|
||||
|
||||
rows, err := gcsql.QuerySQL(query+" ORDER BY DBPREFIXposts.id DESC LIMIT "+strconv.Itoa(limit), args...)
|
||||
query += " ORDER BY DBPREFIXposts.id DESC LIMIT " + strconv.Itoa(limit) + " GROUP BY DBPREFIXposts.id"
|
||||
rows, err := gcsql.QuerySQL(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -41,6 +41,13 @@ func GetThreadFiles(post *Post) ([]Upload, error) {
|
|||
return uploads, nil
|
||||
}
|
||||
|
||||
func (p *Post) nextFileOrder() (int, error) {
|
||||
const query = `SELECT COALESCE(MAX(file_order) + 1, 0) FROM DBPREFIXfiles WHERE post_id = ?`
|
||||
var next int
|
||||
err := QueryRowSQL(query, interfaceSlice(p.ID), interfaceSlice(&next))
|
||||
return next, err
|
||||
}
|
||||
|
||||
func (p *Post) AttachFile(upload *Upload) error {
|
||||
if upload == nil {
|
||||
return nil //
|
||||
|
@ -56,6 +63,12 @@ func (p *Post) AttachFile(upload *Upload) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if upload.FileOrder < 1 {
|
||||
upload.FileOrder, err = p.nextFileOrder()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
upload.PostID = p.ID
|
||||
if _, err = ExecSQL(query,
|
||||
&upload.PostID, &upload.FileOrder, &upload.OriginalFilename, &upload.Filename, &upload.Checksum, &upload.FileSize,
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
|
@ -268,243 +259,27 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
var upload *gcsql.Upload
|
||||
file, handler, err := request.FormFile("imagefile")
|
||||
var filePath, thumbPath, catalogThumbPath string
|
||||
if err != nil || handler.Size == 0 {
|
||||
// no file was uploaded
|
||||
if strings.TrimSpace(post.MessageRaw) == "" {
|
||||
serverutil.ServeErrorPage(writer, "Post must contain a message if no image is uploaded.")
|
||||
return
|
||||
}
|
||||
gcutil.LogAccess(request).
|
||||
Str("post", "referred").
|
||||
Str("referredFrom", request.Referer()).
|
||||
Send()
|
||||
} else {
|
||||
upload = &gcsql.Upload{
|
||||
OriginalFilename: html.EscapeString(handler.Filename),
|
||||
}
|
||||
|
||||
if checkFilenameBan(upload, &post, postBoard, writer, request) {
|
||||
// If checkFilenameBan returns true, an error occured or the file was
|
||||
// rejected for having a banned filename, and the incident was logged either way
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
serverutil.ServeErrorPage(writer, "Error while trying to read file: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Calculate image checksum
|
||||
upload.Checksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
if checkChecksumBan(upload, &post, postBoard, writer, request) {
|
||||
// If checkChecksumBan returns true, an error occured or the file was
|
||||
// rejected for having a banned checksum, and the incident was logged either way
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(upload.OriginalFilename))
|
||||
upload.Filename = getNewFilename() + ext
|
||||
|
||||
boardExists := gcsql.DoesBoardExistByID(
|
||||
gcutil.HackyStringToInt(request.FormValue("boardid")))
|
||||
if !boardExists {
|
||||
serverutil.ServeErrorPage(writer, "No boards have been created yet")
|
||||
return
|
||||
}
|
||||
filePath = path.Join(systemCritical.DocumentRoot, postBoard.Dir, "src", upload.Filename)
|
||||
thumbPath = path.Join(systemCritical.DocumentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("thumb"))
|
||||
catalogThumbPath = path.Join(systemCritical.DocumentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("catalog"))
|
||||
|
||||
if err = os.WriteFile(filePath, data, 0644); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("posting", "upload").
|
||||
Str("filename", upload.Filename).
|
||||
Str("originalFilename", upload.OriginalFilename).
|
||||
Send()
|
||||
serverutil.ServeErrorPage(writer, fmt.Sprintf("Couldn't write file %q", upload.OriginalFilename))
|
||||
return
|
||||
}
|
||||
|
||||
if ext == "webm" || ext == "mp4" {
|
||||
infoEv.Str("post", "withVideo").
|
||||
Str("filename", handler.Filename).
|
||||
Str("referer", request.Referer()).Send()
|
||||
if post.IsTopPost {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidth); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath).
|
||||
Int("thumbWidth", boardConfig.ThumbWidth).
|
||||
Msg("Error creating video thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Error creating video thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidthReply); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath).
|
||||
Int("thumbWidth", boardConfig.ThumbWidthReply).
|
||||
Msg("Error creating video thumbnail for reply")
|
||||
serverutil.ServeErrorPage(writer, "Error creating video thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := createVideoThumbnail(filePath, catalogThumbPath, boardConfig.ThumbWidthCatalog); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath).
|
||||
Int("thumbWidth", boardConfig.ThumbWidthCatalog).
|
||||
Msg("Error creating video thumbnail for catalog")
|
||||
serverutil.ServeErrorPage(writer, "Error creating video thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
|
||||
if err != nil {
|
||||
gcutil.LogError(err).Msg("Error getting video info")
|
||||
|
||||
serverutil.ServeErrorPage(writer, "Error getting video info: "+err.Error())
|
||||
return
|
||||
}
|
||||
if outputBytes != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
for _, line := range outputStringArr {
|
||||
lineArr := strings.Split(line, "=")
|
||||
if len(lineArr) < 2 {
|
||||
continue
|
||||
}
|
||||
value, _ := strconv.Atoi(lineArr[1])
|
||||
switch lineArr[0] {
|
||||
case "width":
|
||||
upload.Width = value
|
||||
case "height":
|
||||
upload.Height = value
|
||||
case "size":
|
||||
upload.FileSize = value
|
||||
}
|
||||
}
|
||||
thumbType := "reply"
|
||||
if post.IsTopPost {
|
||||
thumbType = "op"
|
||||
}
|
||||
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(
|
||||
upload.Width, upload.Height, postBoard.Dir, thumbType)
|
||||
}
|
||||
} else {
|
||||
// Attempt to load uploaded file with imaging library
|
||||
img, err := imaging.Open(filePath)
|
||||
if err != nil {
|
||||
os.Remove(filePath)
|
||||
gcutil.LogError(err).
|
||||
Str("filePath", filePath).Send()
|
||||
serverutil.ServeErrorPage(writer, "Upload filetype not supported")
|
||||
return
|
||||
}
|
||||
// Get image filesize
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("filePath", filePath).Send()
|
||||
serverutil.ServeErrorPage(writer, "Couldn't get image filesize: "+err.Error())
|
||||
return
|
||||
}
|
||||
upload.FileSize = int(stat.Size())
|
||||
|
||||
// Get image width and height, as well as thumbnail width and height
|
||||
upload.Width = img.Bounds().Max.X
|
||||
upload.Height = img.Bounds().Max.Y
|
||||
thumbType := "reply"
|
||||
if post.IsTopPost {
|
||||
thumbType = "op"
|
||||
}
|
||||
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(
|
||||
upload.Width, upload.Height, postBoard.Dir, thumbType)
|
||||
|
||||
gcutil.LogAccess(request).
|
||||
Bool("withFile", true).
|
||||
Str("filename", handler.Filename).
|
||||
Str("referer", request.Referer()).Send()
|
||||
|
||||
if request.FormValue("spoiler") == "on" {
|
||||
// If spoiler is enabled, symlink thumbnail to spoiler image
|
||||
if _, err := os.Stat(path.Join(systemCritical.DocumentRoot, "spoiler.png")); err != nil {
|
||||
serverutil.ServeErrorPage(writer, "missing spoiler.png")
|
||||
return
|
||||
}
|
||||
if err = syscall.Symlink(path.Join(systemCritical.DocumentRoot, "spoiler.png"), thumbPath); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Error creating symbolic link to thumbnail path")
|
||||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
shouldThumb := shouldCreateThumbnail(filePath,
|
||||
upload.Width, upload.Height, upload.ThumbnailWidth, upload.ThumbnailHeight)
|
||||
if shouldThumb {
|
||||
var thumbnail image.Image
|
||||
var catalogThumbnail image.Image
|
||||
if post.IsTopPost {
|
||||
// If this is a new thread, generate thumbnail and catalog thumbnail
|
||||
thumbnail = createImageThumbnail(img, postBoard.Dir, "op")
|
||||
catalogThumbnail = createImageThumbnail(img, postBoard.Dir, "catalog")
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't generate catalog thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
thumbnail = createImageThumbnail(img, postBoard.Dir, "reply")
|
||||
}
|
||||
if err = imaging.Save(thumbnail, thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't save thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If image fits in thumbnail size, symlink thumbnail to original
|
||||
upload.ThumbnailWidth = img.Bounds().Max.X
|
||||
upload.ThumbnailHeight = img.Bounds().Max.Y
|
||||
if err := syscall.Symlink(filePath, thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't create thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
if post.IsTopPost {
|
||||
// Generate catalog thumbnail
|
||||
catalogThumbnail := createImageThumbnail(img, postBoard.Dir, "catalog")
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't generate catalog thumbnail: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boardExists := gcsql.DoesBoardExistByID(boardID)
|
||||
if !boardExists {
|
||||
serverutil.ServeErrorPage(writer, "Board does not exist (invalid boardid)")
|
||||
return
|
||||
}
|
||||
|
||||
upload, gotErr := AttachUploadFromRequest(request, writer, &post, postBoard)
|
||||
if gotErr {
|
||||
// got an error receiving the upload, stop here (assuming an error page was actually shown)
|
||||
return
|
||||
}
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
|
||||
if err = post.Insert(emailCommand != "sage", postBoard.ID, false, false, false, false); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("sql", "postInsertion").
|
||||
Msg("Unable to insert post")
|
||||
if upload != nil {
|
||||
filePath := path.Join(documentRoot, postBoard.Dir, "src", upload.Filename)
|
||||
thumbPath := path.Join(documentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("thumb"))
|
||||
catalogThumbPath := path.Join(documentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("catalog"))
|
||||
os.Remove(filePath)
|
||||
os.Remove(thumbPath)
|
||||
os.Remove(catalogThumbPath)
|
||||
|
@ -517,6 +292,9 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
errEv.Err(err).Caller().
|
||||
Str("sql", "postInsertion").
|
||||
Msg("Unable to attach upload to post")
|
||||
filePath := path.Join(documentRoot, postBoard.Dir, "src", upload.Filename)
|
||||
thumbPath := path.Join(documentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("thumb"))
|
||||
catalogThumbPath := path.Join(documentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("catalog"))
|
||||
os.Remove(filePath)
|
||||
os.Remove(thumbPath)
|
||||
os.Remove(catalogThumbPath)
|
||||
|
|
|
@ -1,22 +1,273 @@
|
|||
package posting
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
"image/gif"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/serverutil"
|
||||
)
|
||||
|
||||
// AttachUploadFromRequest reads an incoming HTTP request and processes any incoming files.
|
||||
// It returns the upload (if there was one) and whether or not any errors were served (meaning
|
||||
// that it should stop processing the post
|
||||
func AttachUploadFromRequest(request *http.Request, writer http.ResponseWriter, post *gcsql.Post, postBoard *gcsql.Board) (*gcsql.Upload, bool) {
|
||||
errEv := gcutil.LogError(nil).
|
||||
Str("IP", post.IP)
|
||||
infoEv := gcutil.LogInfo().
|
||||
Str("IP", post.IP)
|
||||
defer func() {
|
||||
infoEv.Discard()
|
||||
errEv.Discard()
|
||||
}()
|
||||
file, handler, err := request.FormFile("imagefile")
|
||||
if err == http.ErrMissingFile {
|
||||
// no file was submitted with the form
|
||||
return nil, false
|
||||
}
|
||||
wantsJSON := serverutil.IsRequestingJSON(request)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
serverutil.ServeError(writer, err.Error(), wantsJSON, nil)
|
||||
return nil, true
|
||||
}
|
||||
upload := &gcsql.Upload{
|
||||
OriginalFilename: html.EscapeString(handler.Filename),
|
||||
}
|
||||
if checkFilenameBan(upload, post, postBoard, writer, request) {
|
||||
// If checkFilenameBan returns true, an error occured or the file was
|
||||
// rejected for having a banned filename, and the incident was logged either way
|
||||
return nil, true
|
||||
}
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
serverutil.ServeErrorPage(writer, "Error while trying to read file: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Calculate image checksum
|
||||
upload.Checksum = fmt.Sprintf("%x", md5.Sum(data))
|
||||
if checkChecksumBan(upload, post, postBoard, writer, request) {
|
||||
// If checkChecksumBan returns true, an error occured or the file was
|
||||
// rejected for having a banned checksum, and the incident was logged either way
|
||||
return nil, true
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(upload.OriginalFilename))
|
||||
upload.Filename = getNewFilename() + ext
|
||||
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
filePath := path.Join(documentRoot, postBoard.Dir, "src", upload.Filename)
|
||||
thumbPath := path.Join(documentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("thumb"))
|
||||
catalogThumbPath := path.Join(documentRoot, postBoard.Dir, "thumb", upload.ThumbnailPath("catalog"))
|
||||
|
||||
if err = os.WriteFile(filePath, data, 0644); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filename", upload.Filename).
|
||||
Str("originalFilename", upload.OriginalFilename).
|
||||
Send()
|
||||
serverutil.ServeError(writer, fmt.Sprintf("Couldn't write file %q", upload.OriginalFilename), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.Filename,
|
||||
"originalFilename": upload.OriginalFilename,
|
||||
})
|
||||
return nil, true
|
||||
}
|
||||
errEv.
|
||||
Str("filename", handler.Filename).
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath)
|
||||
|
||||
boardConfig := config.GetBoardConfig(postBoard.Dir)
|
||||
if ext == "webm" || ext == "mp4" {
|
||||
infoEv.Str("post", "withVideo").
|
||||
Str("filename", handler.Filename).
|
||||
Str("referer", request.Referer()).Send()
|
||||
if post.IsTopPost {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidth); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath).
|
||||
Int("thumbWidth", boardConfig.ThumbWidth).
|
||||
Msg("Error creating video thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Error creating video thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
} else {
|
||||
if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidthReply); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath).
|
||||
Int("thumbWidth", boardConfig.ThumbWidthReply).
|
||||
Msg("Error creating video thumbnail for reply")
|
||||
serverutil.ServeErrorPage(writer, "Error creating video thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
|
||||
if err := createVideoThumbnail(filePath, catalogThumbPath, boardConfig.ThumbWidthCatalog); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).
|
||||
Str("thumbPath", thumbPath).
|
||||
Int("thumbWidth", boardConfig.ThumbWidthCatalog).
|
||||
Msg("Error creating video thumbnail for catalog")
|
||||
serverutil.ServeErrorPage(writer, "Error creating video thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
|
||||
if err != nil {
|
||||
gcutil.LogError(err).Msg("Error getting video info")
|
||||
serverutil.ServeErrorPage(writer, "Error getting video info: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
if outputBytes != nil {
|
||||
outputStringArr := strings.Split(string(outputBytes), "\n")
|
||||
for _, line := range outputStringArr {
|
||||
lineArr := strings.Split(line, "=")
|
||||
if len(lineArr) < 2 {
|
||||
continue
|
||||
}
|
||||
value, _ := strconv.Atoi(lineArr[1])
|
||||
switch lineArr[0] {
|
||||
case "width":
|
||||
upload.Width = value
|
||||
case "height":
|
||||
upload.Height = value
|
||||
case "size":
|
||||
upload.FileSize = value
|
||||
}
|
||||
}
|
||||
thumbType := "reply"
|
||||
if post.IsTopPost {
|
||||
thumbType = "op"
|
||||
}
|
||||
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(
|
||||
upload.Width, upload.Height, postBoard.Dir, thumbType)
|
||||
}
|
||||
} else {
|
||||
// Attempt to load uploaded file with imaging library
|
||||
img, err := imaging.Open(filePath)
|
||||
if err != nil {
|
||||
os.Remove(filePath)
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
serverutil.ServeErrorPage(writer, "Upload filetype not supported")
|
||||
return nil, true
|
||||
}
|
||||
// Get image filesize
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
serverutil.ServeErrorPage(writer, "Couldn't get image filesize: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
upload.FileSize = int(stat.Size())
|
||||
|
||||
// Get image width and height, as well as thumbnail width and height
|
||||
upload.Width = img.Bounds().Max.X
|
||||
upload.Height = img.Bounds().Max.Y
|
||||
thumbType := "reply"
|
||||
if post.IsTopPost {
|
||||
thumbType = "op"
|
||||
}
|
||||
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(
|
||||
upload.Width, upload.Height, postBoard.Dir, thumbType)
|
||||
|
||||
gcutil.LogAccess(request).
|
||||
Bool("withFile", true).
|
||||
Str("filename", handler.Filename).
|
||||
Str("referer", request.Referer()).Send()
|
||||
|
||||
if request.FormValue("spoiler") == "on" {
|
||||
// If spoiler is enabled, symlink thumbnail to spoiler image
|
||||
if _, err := os.Stat(path.Join(documentRoot, "spoiler.png")); err != nil {
|
||||
serverutil.ServeErrorPage(writer, "missing spoiler.png")
|
||||
return nil, true
|
||||
}
|
||||
if err = syscall.Symlink(path.Join(documentRoot, "spoiler.png"), thumbPath); err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Error creating symbolic link to thumbnail path")
|
||||
serverutil.ServeErrorPage(writer, err.Error())
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
|
||||
shouldThumb := shouldCreateThumbnail(filePath,
|
||||
upload.Width, upload.Height, upload.ThumbnailWidth, upload.ThumbnailHeight)
|
||||
if shouldThumb {
|
||||
var thumbnail image.Image
|
||||
var catalogThumbnail image.Image
|
||||
if post.IsTopPost {
|
||||
// If this is a new thread, generate thumbnail and catalog thumbnail
|
||||
thumbnail = createImageThumbnail(img, postBoard.Dir, "op")
|
||||
catalogThumbnail = createImageThumbnail(img, postBoard.Dir, "catalog")
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't generate catalog thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
} else {
|
||||
thumbnail = createImageThumbnail(img, postBoard.Dir, "reply")
|
||||
}
|
||||
if err = imaging.Save(thumbnail, thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't save thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
} else {
|
||||
// If image fits in thumbnail size, symlink thumbnail to original
|
||||
upload.ThumbnailWidth = img.Bounds().Max.X
|
||||
upload.ThumbnailHeight = img.Bounds().Max.Y
|
||||
if err := syscall.Symlink(filePath, thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't create thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
if post.IsTopPost {
|
||||
// Generate catalog thumbnail
|
||||
catalogThumbnail := createImageThumbnail(img, postBoard.Dir, "catalog")
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
serverutil.ServeErrorPage(writer, "Couldn't generate catalog thumbnail: "+err.Error())
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return upload, false
|
||||
}
|
||||
|
||||
func getBoardThumbnailSize(boardDir string, thumbType string) (int, int) {
|
||||
boardCfg := config.GetBoardConfig(boardDir)
|
||||
switch thumbType {
|
||||
|
|
|
@ -6,16 +6,39 @@
|
|||
|
||||
<form action="/util" method="POST" id="edit-form">
|
||||
<input name="postid" type="hidden" value="{{.post.ID}}" />
|
||||
<input name="boardid" type="hidden" value="{{.boardID}}" />
|
||||
<input name="boardid" type="hidden" value="{{.board.ID}}" />
|
||||
<input name="threadid" type="hidden" value="{{.post.ThreadID}}" />
|
||||
<input name="password" type="hidden" value="{{.post.Password}}" />
|
||||
<input name="doedit" type="hidden" value="1" />
|
||||
<input name="doedit" type="hidden" value="post" />
|
||||
<table id="postbox-static">
|
||||
<tr><th class="postblock">Name</th><td>{{stringAppend .post.Name "!" .post.Tripcode}}</td></tr>
|
||||
<tr><th class="postblock">Email</th><td><input type="email" name="editemail" maxlength="100" size="28" autocomplete="off" value="{{.post.Email}}"/></td></tr>
|
||||
<tr><th class="postblock">Subject</th><td><input type="text" name="editsubject" maxlength="100" size="28" autocomplete="off" value="{{.post.Subject}}"/>
|
||||
<input type="submit" value="Submit changes"/></td></tr>
|
||||
<input type="submit" value="Update"/></td></tr>
|
||||
<tr><th class="postblock">Message</th><td><textarea rows="4" cols="48" name="editmsg" id="editmsg">{{.post.MessageRaw}}</textarea></td></tr>
|
||||
</table>
|
||||
</form><hr/>
|
||||
<header>
|
||||
<h1>{{- with .upload -}}Edit{{else}}Add{{end}} upload</h1>
|
||||
</header>
|
||||
<form action="/util" method="POST" id="upload-form" enctype="multipart/form-data">
|
||||
<input name="postid" type="hidden" value="{{$.post.ID}}" />
|
||||
<input name="boardid" type="hidden" value="{{$.board.ID}}" />
|
||||
<input name="threadid" type="hidden" value="{{$.post.ThreadID}}" />
|
||||
<input name="password" type="hidden" value="{{$.post.Password}}" />
|
||||
<input name="doedit" type="hidden" value="upload" />
|
||||
<table id="postbox-static">
|
||||
{{- with .upload -}}
|
||||
<tr><th class="postblock">Filename</th><td>{{.Filename}}</td></tr>
|
||||
<tr><th class="postblock">Thumbnail</th><td>
|
||||
<img src="{{webPath $.board.Dir "thumb" (.ThumbnailPath "reply")}}" alt="{{webPath $.board.Dir "src" .Filename}}" width="{{.ThumbnailWidth}}" height="{{.ThumbnailHeight}}" class="upload" />
|
||||
</td></tr>
|
||||
{{- end -}}
|
||||
<tr><th>Spoiler</th><td><input type="checkbox" name="spoiler" id="spoiler" {{with .upload}}{{if .IsSpoilered}}checked{{end}}{{end}}></td></tr>
|
||||
<tr><th>Replace</th><td>
|
||||
<input name="imagefile" type="file" accept="image/jpeg,image/png,image/gif,video/webm,video/mp4" onchange="var sub = document.getElementById('update-file'); if(this.value != '') { sub.disabled = false; sub.value = 'Update file'; } else { sub.disabled = true; sub.value = 'No file selected'}"/>
|
||||
</td></tr>
|
||||
</table>
|
||||
<div style="text-align: center;"><input type="submit" id="update-file" value="Update file" onclick="confirm()"></div>
|
||||
</form><br />
|
||||
{{template "page_footer.html" .}}
|
Loading…
Add table
Add a link
Reference in a new issue