1
0
Fork 0
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:
Eggbertx 2023-07-12 14:20:41 -07:00
parent 2bac71f828
commit 4078197b8d
10 changed files with 521 additions and 455 deletions

View file

@ -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

View file

@ -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 == "" {

View file

@ -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

View file

@ -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
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View file

@ -1,4 +1,4 @@
package posting
package uploads
import (
"errors"