mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-02 15:06:23 -07:00
Move upload handling to subpackage
This commit is contained in:
parent
2bac71f828
commit
4078197b8d
10 changed files with 521 additions and 455 deletions
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/gochan-org/gochan/pkg/manage"
|
||||
"github.com/gochan-org/gochan/pkg/posting"
|
||||
"github.com/gochan-org/gochan/pkg/posting/uploads"
|
||||
"github.com/gochan-org/gochan/pkg/server"
|
||||
"github.com/gochan-org/gochan/pkg/server/serverutil"
|
||||
)
|
||||
|
@ -147,7 +148,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
return
|
||||
}
|
||||
|
||||
upload, gotErr := posting.AttachUploadFromRequest(request, writer, post, board)
|
||||
upload, gotErr := uploads.AttachUploadFromRequest(request, writer, post, board)
|
||||
if gotErr {
|
||||
// AttachUploadFromRequest handles error serving/logging
|
||||
return
|
||||
|
|
|
@ -94,49 +94,6 @@ func checkUsernameBan(post *gcsql.Post, postBoard *gcsql.Board, writer http.Resp
|
|||
return true
|
||||
}
|
||||
|
||||
func checkFilenameBan(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
|
||||
filenameBan, err := gcsql.CheckFilenameBan(upload.OriginalFilename, postBoard.ID)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", post.IP).
|
||||
Str("filename", upload.OriginalFilename).
|
||||
Str("boardDir", postBoard.Dir).
|
||||
Msg("Error getting name banned status")
|
||||
server.ServeErrorPage(writer, "Error getting filename ban info")
|
||||
return true
|
||||
}
|
||||
if filenameBan == nil {
|
||||
return false
|
||||
}
|
||||
server.ServeError(writer, "Filename not allowed", serverutil.IsRequestingJSON(request), map[string]interface{}{})
|
||||
gcutil.LogWarning().
|
||||
Str("originalFilename", upload.OriginalFilename).
|
||||
Msg("File rejected for having a banned filename")
|
||||
return true
|
||||
}
|
||||
|
||||
func checkChecksumBan(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
|
||||
fileBan, err := gcsql.CheckFileChecksumBan(upload.Checksum, postBoard.ID)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", post.IP).
|
||||
Str("boardDir", postBoard.Dir).
|
||||
Str("checksum", upload.Checksum).
|
||||
Msg("Error getting file checksum ban status")
|
||||
server.ServeErrorPage(writer, "Error processing file: "+err.Error())
|
||||
return true
|
||||
}
|
||||
if fileBan == nil {
|
||||
return false
|
||||
}
|
||||
server.ServeError(writer, "File not allowed", serverutil.IsRequestingJSON(request), map[string]interface{}{})
|
||||
gcutil.LogWarning().
|
||||
Str("originalFilename", upload.OriginalFilename).
|
||||
Str("checksum", upload.Checksum).
|
||||
Msg("File rejected for having a banned checksum")
|
||||
return true
|
||||
}
|
||||
|
||||
func handleAppeal(writer http.ResponseWriter, request *http.Request, errEv *zerolog.Event) {
|
||||
banIDstr := request.FormValue("banid")
|
||||
if banIDstr == "" {
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"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/posting/uploads"
|
||||
"github.com/gochan-org/gochan/pkg/server"
|
||||
"github.com/gochan-org/gochan/pkg/server/serverutil"
|
||||
)
|
||||
|
@ -290,7 +291,7 @@ func MakePost(writer http.ResponseWriter, request *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
upload, gotErr := AttachUploadFromRequest(request, writer, post, postBoard)
|
||||
upload, gotErr := uploads.AttachUploadFromRequest(request, writer, post, postBoard)
|
||||
if gotErr {
|
||||
// got an error receiving the upload, stop here (assuming an error page was actually shown)
|
||||
return
|
||||
|
|
|
@ -1,409 +0,0 @@
|
|||
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.ServeError(writer, "Error while trying to read file: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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 {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Unable to save upload (recovered from a panic in event handler)", wantsJSON,
|
||||
map[string]interface{}{"event": "upload-saved"})
|
||||
return nil, true
|
||||
}
|
||||
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.ServeError(writer, "Error creating video thumbnail: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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.ServeError(writer, "Error creating video thumbnail: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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.ServeError(writer, "Error creating video thumbnail: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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.ServeError(writer, "Error getting video info: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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 := ThumbnailReply
|
||||
if post.ThreadID == 0 {
|
||||
thumbType = ThumbnailOP
|
||||
}
|
||||
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.ServeError(writer, "Couldn't get upload filesize: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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.ServeError(writer, "Upload filetype not supported", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
return nil, true
|
||||
}
|
||||
// Get image filesize
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
server.ServeError(writer, "Couldn't get image filesize", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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 := ThumbnailReply
|
||||
if post.ThreadID == 0 {
|
||||
thumbType = ThumbnailOP
|
||||
}
|
||||
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.ServeError(writer, "missing spoiler.png", wantsJSON, nil)
|
||||
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.ServeError(writer, "Failed creating spoiler thumbnail", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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, ThumbnailOP)
|
||||
catalogThumbnail = createImageThumbnail(img, postBoard.Dir, ThumbnailCatalog)
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
server.ServeError(writer, "Couldn't generate catalog thumbnail", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
return nil, true
|
||||
}
|
||||
} else {
|
||||
thumbnail = createImageThumbnail(img, postBoard.Dir, ThumbnailReply)
|
||||
}
|
||||
if err = imaging.Save(thumbnail, thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
server.ServeError(writer, "Couldn't save thumbnail", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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.ServeError(writer, "Couldn't create thumbnail", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
return nil, true
|
||||
}
|
||||
if post.ThreadID == 0 {
|
||||
// Generate catalog thumbnail
|
||||
catalogThumbnail := createImageThumbnail(img, postBoard.Dir, ThumbnailCatalog)
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
server.ServeError(writer, "Couldn't generate catalog thumbnail", wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
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 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
|
||||
}
|
187
pkg/posting/uploads/attach.go
Normal file
187
pkg/posting/uploads/attach.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package uploads
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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 IsFilenameBanned(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.ServeError(writer, "Error while trying to read file: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
return nil, true
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Calculate image checksum
|
||||
upload.Checksum = fmt.Sprintf("%x", md5.Sum(data)) // skipcq: GSC-G401
|
||||
if IsChecksumBanned(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, errEv, infoEv)
|
||||
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
|
||||
}
|
||||
|
||||
// event triggered after the file is successfully written but be
|
||||
_, err, recovered := events.TriggerEvent("upload-saved", filePath)
|
||||
if recovered {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Unable to save upload (recovered from a panic in event handler)", wantsJSON,
|
||||
map[string]interface{}{"event": "upload-saved"})
|
||||
return nil, true
|
||||
}
|
||||
if err != nil {
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.Filename,
|
||||
"originalFilename": upload.OriginalFilename,
|
||||
})
|
||||
return nil, true
|
||||
}
|
||||
|
||||
infoEv.Str("referer", request.Referer()).Str("filename", handler.Filename).Send()
|
||||
access := gcutil.LogAccess(request).
|
||||
Str("filename", handler.Filename).
|
||||
Str("referer", request.Referer())
|
||||
|
||||
upload.IsSpoilered = request.FormValue("spoiler") == "on"
|
||||
switch ext {
|
||||
// images
|
||||
case ".gif":
|
||||
fallthrough
|
||||
case ".jpg":
|
||||
fallthrough
|
||||
case ".jpeg":
|
||||
fallthrough
|
||||
case ".png":
|
||||
fallthrough
|
||||
case ".webp":
|
||||
if err = processImage(upload, post, postBoard.Dir, filePath, thumbPath, catalogThumbPath, infoEv, errEv); err != nil {
|
||||
server.ServeError(writer, "Error processing image: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
}
|
||||
access.Str("handler", "image").Send()
|
||||
// videos
|
||||
case ".mp4":
|
||||
fallthrough
|
||||
case ".webm":
|
||||
if err = processVideo(upload, post, postBoard.Dir, filePath, thumbPath, catalogThumbPath, infoEv, errEv); err != nil {
|
||||
server.ServeError(writer, "Error processing video: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
}
|
||||
access.Str("handler", "video").Send()
|
||||
default:
|
||||
// other (.pdf, .zip, etc)
|
||||
if err = processOther(upload, post, postBoard.Dir, filePath, thumbPath, catalogThumbPath, infoEv, errEv); err != nil {
|
||||
server.ServeError(writer, "Error processing file: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
}
|
||||
access.Str("handler", "other").Send()
|
||||
}
|
||||
|
||||
return upload, false
|
||||
}
|
||||
|
||||
func getNewFilename() string {
|
||||
now := time.Now().Unix()
|
||||
// rand.Seed(now)
|
||||
return strconv.Itoa(int(now)) + strconv.Itoa(rand.Intn(98)+1)
|
||||
}
|
53
pkg/posting/uploads/filebans.go
Normal file
53
pkg/posting/uploads/filebans.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package uploads
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func IsFilenameBanned(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
|
||||
filenameBan, err := gcsql.CheckFilenameBan(upload.OriginalFilename, postBoard.ID)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", post.IP).
|
||||
Str("filename", upload.OriginalFilename).
|
||||
Str("boardDir", postBoard.Dir).
|
||||
Msg("Error getting name banned status")
|
||||
server.ServeErrorPage(writer, "Error getting filename ban info")
|
||||
return true
|
||||
}
|
||||
if filenameBan == nil {
|
||||
return false
|
||||
}
|
||||
server.ServeError(writer, "Filename not allowed", serverutil.IsRequestingJSON(request), map[string]interface{}{})
|
||||
gcutil.LogWarning().
|
||||
Str("originalFilename", upload.OriginalFilename).
|
||||
Msg("File rejected for having a banned filename")
|
||||
return true
|
||||
}
|
||||
|
||||
func IsChecksumBanned(upload *gcsql.Upload, post *gcsql.Post, postBoard *gcsql.Board, writer http.ResponseWriter, request *http.Request) bool {
|
||||
fileBan, err := gcsql.CheckFileChecksumBan(upload.Checksum, postBoard.ID)
|
||||
if err != nil {
|
||||
gcutil.LogError(err).
|
||||
Str("IP", post.IP).
|
||||
Str("boardDir", postBoard.Dir).
|
||||
Str("checksum", upload.Checksum).
|
||||
Msg("Error getting file checksum ban status")
|
||||
server.ServeErrorPage(writer, "Error processing file: "+err.Error())
|
||||
return true
|
||||
}
|
||||
if fileBan == nil {
|
||||
return false
|
||||
}
|
||||
server.ServeError(writer, "File not allowed", serverutil.IsRequestingJSON(request), map[string]interface{}{})
|
||||
gcutil.LogWarning().
|
||||
Str("originalFilename", upload.OriginalFilename).
|
||||
Str("checksum", upload.Checksum).
|
||||
Msg("File rejected for having a banned checksum")
|
||||
return true
|
||||
}
|
137
pkg/posting/uploads/processimage.go
Normal file
137
pkg/posting/uploads/processimage.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package uploads
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/gif"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
func processImage(upload *gcsql.Upload, post *gcsql.Post, board string, filePath string, thumbPath string, catalogThumbPath string, infoEv *zerolog.Event, errEv *zerolog.Event) error {
|
||||
img, err := imaging.Open(filePath)
|
||||
if err != nil {
|
||||
os.Remove(filePath)
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
return err
|
||||
}
|
||||
// Get image filesize
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
return err
|
||||
}
|
||||
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 := ThumbnailReply
|
||||
if post.ThreadID == 0 {
|
||||
thumbType = ThumbnailOP
|
||||
}
|
||||
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(upload.Width, upload.Height, board, thumbType)
|
||||
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
if upload.IsSpoilered {
|
||||
// If spoiler is enabled, symlink thumbnail to spoiler image
|
||||
if _, err := os.Stat(path.Join(documentRoot, "spoiler.png")); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return err
|
||||
}
|
||||
if err = syscall.Symlink(path.Join(documentRoot, "spoiler.png"), thumbPath); err != nil {
|
||||
errEv.Err(err).
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Error creating symbolic link to thumbnail path")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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, board, ThumbnailOP)
|
||||
catalogThumbnail = createImageThumbnail(img, board, ThumbnailCatalog)
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
thumbnail = createImageThumbnail(img, board, ThumbnailReply)
|
||||
}
|
||||
if err = imaging.Save(thumbnail, thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
return err
|
||||
}
|
||||
} 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")
|
||||
return err
|
||||
}
|
||||
if post.ThreadID == 0 {
|
||||
// Generate catalog thumbnail
|
||||
catalogThumbnail := createImageThumbnail(img, board, ThumbnailCatalog)
|
||||
if err = imaging.Save(catalogThumbnail, catalogThumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", catalogThumbPath).
|
||||
Msg("Couldn't generate catalog thumbnail")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
66
pkg/posting/uploads/processother.go
Normal file
66
pkg/posting/uploads/processother.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package uploads
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedFileExt = errors.New("unsupported file extension")
|
||||
)
|
||||
|
||||
func processOther(upload *gcsql.Upload, post *gcsql.Post, board string, filePath string, thumbPath string, catalogThumbPath string, infoEv *zerolog.Event, errEv *zerolog.Event) error {
|
||||
boardConfig := config.GetBoardConfig(board)
|
||||
ext := path.Ext(filePath)
|
||||
cfgThumb, ok := boardConfig.AllowOtherExtensions[ext]
|
||||
if !ok {
|
||||
errEv.Err(ErrUnsupportedFileExt).Str("ext", ext).Caller().Send()
|
||||
return ErrUnsupportedFileExt
|
||||
}
|
||||
infoEv.Str("post", "withOther")
|
||||
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
return err
|
||||
}
|
||||
|
||||
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(config.GetSystemCriticalConfig().DocumentRoot, staticThumbPath)
|
||||
if _, err = os.Stat(originalThumbPath); err != nil {
|
||||
errEv.Err(err).Str("originalThumbPath", originalThumbPath).Send()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Symlink(originalThumbPath, thumbPath); err != nil {
|
||||
os.Remove(filePath)
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
return err
|
||||
}
|
||||
if post.ThreadID == 0 {
|
||||
if err = os.Symlink(originalThumbPath, catalogThumbPath); err != nil {
|
||||
os.Remove(filePath)
|
||||
errEv.Err(err).Caller().
|
||||
Str("filePath", filePath).Send()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
73
pkg/posting/uploads/processvideo.go
Normal file
73
pkg/posting/uploads/processvideo.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package uploads
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func processVideo(upload *gcsql.Upload, post *gcsql.Post, board string, filePath string, thumbPath string, catalogThumbPath string, infoEv *zerolog.Event, errEv *zerolog.Event) error {
|
||||
boardConfig := config.GetBoardConfig(board)
|
||||
infoEv.Str("post", "withVideo")
|
||||
var err error
|
||||
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")
|
||||
return err
|
||||
}
|
||||
} 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")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
return err
|
||||
}
|
||||
|
||||
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
|
||||
if err != nil {
|
||||
gcutil.LogError(err).Msg("Error getting video info")
|
||||
return err
|
||||
}
|
||||
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 := ThumbnailReply
|
||||
if post.ThreadID == 0 {
|
||||
thumbType = ThumbnailOP
|
||||
}
|
||||
upload.ThumbnailWidth, upload.ThumbnailHeight = getThumbnailSize(
|
||||
upload.Width, upload.Height, board, thumbType)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package posting
|
||||
package uploads
|
||||
|
||||
import (
|
||||
"errors"
|
Loading…
Add table
Add a link
Reference in a new issue