1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-20 01:06:24 -07:00
gochan/pkg/posting/uploads.go
2023-06-02 15:16:17 -07:00

403 lines
13 KiB
Go

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/events"
"github.com/gochan-org/gochan/pkg/gcsql"
"github.com/gochan-org/gochan/pkg/gcutil"
"github.com/gochan-org/gochan/pkg/server"
"github.com/gochan-org/gochan/pkg/server/serverutil"
_ "golang.org/x/image/webp"
)
// 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()
}()
wantsJSON := serverutil.IsRequestingJSON(request)
file, handler, err := request.FormFile("imagefile")
if errors.Is(err, http.ErrMissingFile) {
// no file was submitted with the form
return nil, false
}
if err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, err.Error(), wantsJSON, nil)
return nil, true
}
upload := &gcsql.Upload{
OriginalFilename: html.EscapeString(handler.Filename),
}
gcutil.LogStr("originalFilename", upload.OriginalFilename, errEv, infoEv)
boardConfig := config.GetBoardConfig(postBoard.Dir)
if !boardConfig.AcceptedExtension(upload.OriginalFilename) {
errEv.Caller().Msg("Upload filetype not supported")
server.ServeError(writer, "Upload filetype not supported", wantsJSON, map[string]interface{}{
"filename": upload.OriginalFilename,
})
return nil, true
}
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()
server.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)) // skipcq: GSC-G401
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"))
errEv.
Str("originalFilename", upload.OriginalFilename).
Str("filePath", filePath)
if post.ThreadID == 0 {
errEv.Str("catalogThumbPath", catalogThumbPath)
}
if err = os.WriteFile(filePath, data, config.GC_FILE_MODE); err != nil {
errEv.Err(err).Caller().Send()
server.ServeError(writer, fmt.Sprintf("Couldn't write file %q", upload.OriginalFilename), wantsJSON, map[string]interface{}{
"filename": upload.Filename,
"originalFilename": upload.OriginalFilename,
})
return nil, true
}
gcutil.LogStr("stripImageMetadata", boardConfig.StripImageMetadata)
if err = stripImageMetadata(filePath, boardConfig); err != nil {
errEv.Err(err).Caller().Msg("Unable to strip metadata")
server.ServeError(writer, "Unable to strip metadata from image", wantsJSON, map[string]interface{}{
"filename": upload.Filename,
"originalFilename": upload.OriginalFilename,
})
return nil, true
}
_, err, recovered := events.TriggerEvent("upload-saved", filePath)
if recovered {
gcutil.LogWarning().Caller().
Str("filePath", filePath).Str("triggeredEvent", "upload-saved").
Msg("Recovered from a panic in event handler")
}
if err != nil {
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
"filename": upload.Filename,
"originalFilename": upload.OriginalFilename,
})
return nil, true
}
if ext == ".webm" || ext == ".mp4" {
infoEv.Str("post", "withVideo").
Str("filename", handler.Filename).
Str("referer", request.Referer()).Send()
if post.ThreadID == 0 {
if err := createVideoThumbnail(filePath, thumbPath, boardConfig.ThumbWidth); err != nil {
errEv.Err(err).Caller().
Int("thumbWidth", boardConfig.ThumbWidth).
Msg("Error creating video thumbnail")
server.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("thumbPath", thumbPath).
Int("thumbWidth", boardConfig.ThumbWidthReply).
Msg("Error creating video thumbnail for reply")
server.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("thumbPath", thumbPath).
Int("thumbWidth", boardConfig.ThumbWidthCatalog).
Msg("Error creating video thumbnail for catalog")
server.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")
server.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.ThreadID == 0 {
thumbType = "op"
}
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(
upload.Width, upload.Height, postBoard.Dir, thumbType)
}
} else if cfgThumb, ok := boardConfig.AllowOtherExtensions[ext]; ok {
stat, err := os.Stat(filePath)
if err != nil {
errEv.Err(err).Caller().
Str("filePath", filePath).Send()
server.ServeErrorPage(writer, "Couldn't get upload filesize: "+err.Error())
return nil, true
}
upload.FileSize = int(stat.Size())
if post.ThreadID == 0 {
// OP
upload.ThumbnailWidth = boardConfig.ThumbWidth
upload.ThumbnailHeight = boardConfig.ThumbHeight
} else {
// reply
upload.ThumbnailWidth = boardConfig.ThumbWidthReply
upload.ThumbnailHeight = boardConfig.ThumbHeightReply
}
staticThumbPath := path.Join("static/", cfgThumb)
originalThumbPath := path.Join(documentRoot, staticThumbPath)
if _, err = os.Stat(originalThumbPath); err != nil {
errEv.Err(err).Str("originalThumbPath", originalThumbPath).Send()
server.ServeError(writer, "missing static thumbnail "+staticThumbPath, wantsJSON, nil)
return nil, true
}
if err = os.Symlink(originalThumbPath, thumbPath); err != nil {
os.Remove(filePath)
errEv.Err(err).Caller().
Str("filePath", filePath).Send()
server.ServeError(writer, "Failed creating symbolic link to thumbnail path", wantsJSON, nil)
return nil, true
}
if post.ThreadID == 0 {
if err = os.Symlink(originalThumbPath, catalogThumbPath); err != nil {
os.Remove(filePath)
errEv.Err(err).Caller().
Str("filePath", filePath).Send()
server.ServeError(writer, "Failed creating symbolic link to thumbnail path", wantsJSON, nil)
return nil, true
}
}
} 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()
server.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()
server.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.ThreadID == 0 {
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 {
server.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")
server.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.ThreadID == 0 {
// 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")
server.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")
server.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")
server.ServeErrorPage(writer, "Couldn't create thumbnail: "+err.Error())
return nil, true
}
if post.ThreadID == 0 {
// 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")
server.ServeErrorPage(writer, "Couldn't generate catalog thumbnail: "+err.Error())
return nil, true
}
}
}
}
return upload, false
}
func stripImageMetadata(filePath string, boardConfig *config.BoardConfig) (err error) {
var stripFlag string
switch boardConfig.StripImageMetadata {
case "exif":
stripFlag = "-EXIF="
case "all":
stripFlag = "-all="
case "none":
fallthrough
case "":
return nil
}
err = exec.Command(boardConfig.ExiftoolPath, "-overwrite_original_in_place", stripFlag, filePath).Run()
return
}
// func getVideoInfo(path string) (map[string]int, error) {
// vidInfo := make(map[string]int)
// outputBytes, err := exec.Command("ffprobe", "-v quiet", "-show_format", "-show_streams", path).CombinedOutput()
// if err == nil && outputBytes != nil {
// outputStringArr := strings.Split(string(outputBytes), "\n")
// for _, line := range outputStringArr {
// lineArr := strings.Split(line, "=")
// if len(lineArr) < 2 {
// continue
// }
// if lineArr[0] == "width" || lineArr[0] == "height" || lineArr[0] == "size" {
// value, _ := strconv.Atoi(lineArr[1])
// vidInfo[lineArr[0]] = value
// }
// }
// }
// return vidInfo, err
// }
func getNewFilename() string {
now := time.Now().Unix()
rand.Seed(now)
return strconv.Itoa(int(now)) + strconv.Itoa(rand.Intn(98)+1)
}
func numImageFrames(imgPath string) (int, error) {
if path.Ext(imgPath) != ".gif" {
return 1, nil
}
fi, err := os.Open(imgPath)
if err != nil {
return 0, err
}
defer fi.Close()
g, err := gif.DecodeAll(fi)
if err != nil {
return 0, err
}
return len(g.Image), nil
}