diff options
| author | Bobby <[email protected]> | 2025-07-17 12:39:44 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-07-17 12:39:44 +0530 |
| commit | 46e41088bab946856867253c4fa268f9084b86a5 (patch) | |
| tree | 617b5b31de5b2c0fe0cfd27f29b5580a784d468f | |
| parent | b0ba363696a758a8d0637107bd29a0a9ac1382d4 (diff) | |
| download | imageboard-46e41088bab946856867253c4fa268f9084b86a5.tar.xz imageboard-46e41088bab946856867253c4fa268f9084b86a5.zip | |
webp support, decode and transformation functions
| -rw-r--r-- | controllers/posts.go | 68 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 10 | ||||
| -rw-r--r-- | imageboard/main.go | 1 | ||||
| -rw-r--r-- | utils/format/files.go | 71 | ||||
| -rw-r--r-- | utils/format/format.go | 32 | ||||
| -rw-r--r-- | utils/format/numbers.go | 20 | ||||
| -rw-r--r-- | utils/format/time.go | 7 | ||||
| -rw-r--r-- | utils/transformers/image.go | 73 | ||||
| -rw-r--r-- | utils/transformers/tokens.go | 7 | ||||
| -rw-r--r-- | utils/validators/url.go | 26 |
11 files changed, 273 insertions, 47 deletions
diff --git a/controllers/posts.go b/controllers/posts.go index 2ebd6b2..dd612fb 100644 --- a/controllers/posts.go +++ b/controllers/posts.go @@ -1,8 +1,10 @@ package controllers
import (
+ "image"
"imageboard/config"
"imageboard/database"
+ "imageboard/models"
"imageboard/utils/auth"
"imageboard/utils/format"
"imageboard/utils/shortcuts"
@@ -100,27 +102,79 @@ func PostsUploadPostController(ctx *fiber.Ctx) error { imageFile := imageFiles[0]
- rating := ctx.FormValue("rating")
- if rating == "" {
- rating = "safe"
+ contentType := imageFile.Header.Get("Content-Type")
+ if !strings.HasPrefix(contentType, "image/") {
+ return fiber.NewError(fiber.StatusBadRequest, "Uploaded file is not an image")
}
- sourceURL := ctx.FormValue("source_url")
+ if !strings.Contains(config.Upload.AllowedTypes, contentType) {
+ return fiber.NewError(fiber.StatusBadRequest, "Uploaded image type is not allowed")
+ }
- // Validate file size
maxSize := int64(config.Upload.MaxSize)
if imageFile.Size > maxSize {
return fiber.NewError(fiber.StatusRequestEntityTooLarge,
"File size exceeds maximum allowed size of "+format.FileSize(maxSize))
}
- // Validate content type
+ sourceURL := ctx.FormValue("source_url")
+
file, err := imageFile.Open()
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to open uploaded file")
}
defer file.Close()
+ imageData, err := io.ReadAll(file)
+ if err != nil {
+ return fiber.NewError(fiber.StatusInternalServerError, "Failed to read uploaded file")
+ }
+
+ decodedImage, format, err := format.DecodeImage(imageData)
+ if err != nil {
+ return fiber.NewError(fiber.StatusInternalServerError, "Failed to decode image: "+err.Error())
+ }
+
+ var fileName string
+ if sourceURL != "" {
+ fileName = transformers.CreateUniqueFileName(sourceURL, format)
+ } else {
+ fileName = transformers.CreateUniqueFileName(imageFile.Filename, format)
+ }
+
+ rating := ctx.FormValue("rating")
+ if rating == "" {
+ rating = "safe"
+ }
+
+ imageSizes := make(map[config.ImageSizeType]struct {
+ imageSize models.ImageSize
+ image image.Image
+ })
+ isizeArray := []config.ImageSizeType{
+ config.ImageSizeTypeIcon,
+ config.ImageSizeTypeThumbnail,
+ config.ImageSizeTypeSmall,
+ config.ImageSizeTypeMedium,
+ config.ImageSizeTypeLarge,
+ config.ImageSizeTypeOriginal,
+ }
+
+ for _, sizeType := range isizeArray {
+ size, img, err := transformers.TransformImageToVariant(decodedImage, sizeType)
+ if err != nil {
+ return fiber.NewError(fiber.StatusInternalServerError, "Failed to process image: "+err.Error())
+ }
+ size.ImageID = 0 // This will be set later when saving the image
+ imageSizes[sizeType] = struct {
+ imageSize models.ImageSize
+ image image.Image
+ }{
+ imageSize: size,
+ image: img,
+ }
+ }
+
// For now, just return success - in a full implementation:
// 1. Generate unique filename
// 2. Calculate MD5 hash
@@ -133,7 +187,7 @@ func PostsUploadPostController(ctx *fiber.Ctx) error { "success": true,
"message": "Image uploaded successfully",
"data": fiber.Map{
- "filename": imageFile.Filename,
+ "filename": fileName,
"size": imageFile.Size,
"rating": rating,
"source_url": sourceURL,
@@ -8,6 +8,7 @@ require ( github.com/gofiber/template/django/v3 v3.1.14 github.com/joho/godotenv v1.5.1 golang.org/x/crypto v0.31.0 + golang.org/x/image v0.29.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.30.0 ) @@ -33,7 +34,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.27.0 // indirect ) @@ -63,14 +63,16 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas= +golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/imageboard/main.go b/imageboard/main.go index eef73ae..57c5eea 100644 --- a/imageboard/main.go +++ b/imageboard/main.go @@ -29,7 +29,6 @@ func main() { app := fiber.New(fiber.Config{
Views: engine,
ErrorHandler: handlers.ServerErrorHandler,
- BodyLimit: config.Upload.MaxSize,
})
app.Use(recover.New())
diff --git a/utils/format/files.go b/utils/format/files.go new file mode 100644 index 0000000..82f1546 --- /dev/null +++ b/utils/format/files.go @@ -0,0 +1,71 @@ +package format + +import ( + "bytes" + "fmt" + "image" + _ "image/gif" + "image/jpeg" + "image/png" + "strings" + + "golang.org/x/image/webp" +) + +func init() { + image.RegisterFormat("webp", "RIFF????WEBP", webp.Decode, webp.DecodeConfig) +} + +func FileSize(size int64) string { + const unit = 1024 + if size < unit { + return fmt.Sprintf("%d B", size) + } + div, exp := int64(unit), 0 + for n := size / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + + val := float64(size) / float64(div) + unitStr := "KMGTPE"[exp : exp+1] + if val == float64(int64(val)) { + return fmt.Sprintf("%d %sB", int64(val), unitStr) + } + return fmt.Sprintf("%.2f %sB", val, unitStr) +} + +func RemoveExtension(fileName string) string { + if fileName == "" { + return fileName + } + parts := strings.Split(fileName, ".") + if len(parts) <= 1 { + return fileName + } + return strings.Join(parts[:len(parts)-1], ".") +} + +func DecodeImage(imgData []byte) (image.Image, string, error) { + img, formatName, err := image.Decode(bytes.NewReader(imgData)) + return img, formatName, err +} + +func GetImageFileSize(img image.Image) int64 { + var buf bytes.Buffer + switch img.(type) { + case *image.NRGBA, *image.RGBA, *image.YCbCr: + // Use PNG encoding for lossless compression + err := png.Encode(&buf, img) + if err != nil { + return 0 + } + default: + // Fallback to JPEG encoding for other formats + err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 85}) + if err != nil { + return 0 + } + } + return int64(buf.Len()) +} diff --git a/utils/format/format.go b/utils/format/format.go deleted file mode 100644 index e57bc1f..0000000 --- a/utils/format/format.go +++ /dev/null @@ -1,32 +0,0 @@ -package format - -import "fmt" - -func FileSize(size int64) string { - const unit = 1024 - if size < unit { - return fmt.Sprintf("%d B", size) - } - div, exp := int64(unit), 0 - for n := size / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - - val := float64(size) / float64(div) - unitStr := "KMGTPE"[exp : exp+1] - if val == float64(int64(val)) { - return fmt.Sprintf("%d %sB", int64(val), unitStr) - } - return fmt.Sprintf("%.2f %sB", val, unitStr) -} - -func Count(count int64) string { - if count < 1000 { - return fmt.Sprintf("%d", count) - } else if count < 1000000 { - return fmt.Sprintf("%.1fK", float64(count)/1000) - } else { - return fmt.Sprintf("%.1fM", float64(count)/1000000) - } -} diff --git a/utils/format/numbers.go b/utils/format/numbers.go new file mode 100644 index 0000000..8f546f1 --- /dev/null +++ b/utils/format/numbers.go @@ -0,0 +1,20 @@ +package format + +import "fmt" + +func Count(count int64) string { + if count < 1000 { + return fmt.Sprintf("%d", count) + } else if count < 1000000 { + return fmt.Sprintf("%.1fK", float64(count)/1000) + } else { + return fmt.Sprintf("%.1fM", float64(count)/1000000) + } +} + +func Int64ToString(value int64) string { + if value < 0 { + return fmt.Sprintf("-%d", -value) + } + return fmt.Sprintf("%d", value) +} diff --git a/utils/format/time.go b/utils/format/time.go new file mode 100644 index 0000000..3e65337 --- /dev/null +++ b/utils/format/time.go @@ -0,0 +1,7 @@ +package format + +import "time" + +func GetCurrentTimeAsTimestamp() int64 { + return time.Now().Unix() +} diff --git a/utils/transformers/image.go b/utils/transformers/image.go index a88d8bb..e392f7f 100644 --- a/utils/transformers/image.go +++ b/utils/transformers/image.go @@ -1,6 +1,77 @@ package transformers -import "imageboard/config" +import ( + "image" + "imageboard/config" + "imageboard/models" + "imageboard/utils/format" + "imageboard/utils/validators" + "strings" +) + +func TransformImageToVariant(img image.Image, variant config.ImageSizeType) (models.ImageSize, image.Image, error) { + variantSizeMap := map[config.ImageSizeType]int{ + config.ImageSizeTypeIcon: 64, + config.ImageSizeTypeThumbnail: 256, + config.ImageSizeTypeSmall: 512, + config.ImageSizeTypeMedium: 1024, + config.ImageSizeTypeLarge: 2048, + config.ImageSizeTypeOriginal: 0, // Original size, no resizing + } + + maxWidth := variantSizeMap[variant] + if maxWidth > 0 { + img = ResizeImage(img, maxWidth) + } + + fileSize := format.GetImageFileSize(img) + + return models.ImageSize{ + SizeType: variant, + Width: img.Bounds().Dx(), + Height: img.Bounds().Dy(), + FileSize: fileSize, + }, img, nil +} + +func ResizeImage(img image.Image, maxWidth int) image.Image { + if maxWidth <= 0 || img.Bounds().Dx() <= maxWidth { + return img + } + + ratio := float64(maxWidth) / float64(img.Bounds().Dx()) + newWidth := int(float64(img.Bounds().Dx()) * ratio) + newHeight := int(float64(img.Bounds().Dy()) * ratio) + newImg := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) + for y := 0; y < newHeight; y++ { + for x := 0; x < newWidth; x++ { + srcX := int(float64(x) / ratio) + srcY := int(float64(y) / ratio) + if srcX < img.Bounds().Dx() && srcY < img.Bounds().Dy() { + newImg.Set(x, y, img.At(srcX, srcY)) + } + } + } + return newImg +} + +func CreateUniqueFileName(sourceURLOrOriginalName, imageFormat string) string { + fileName := sourceURLOrOriginalName + if validators.IsValidURL(sourceURLOrOriginalName) { + parts := strings.Split(sourceURLOrOriginalName, "/") + fileName = parts[len(parts)-1] + } + + currentTime := format.GetCurrentTimeAsTimestamp() + fileNameWithoutExtension := format.RemoveExtension(fileName) + fileName = GenerateTokenFromString(fileNameWithoutExtension + "_" + format.Int64ToString(currentTime)) + + if len(fileName) > 32 { + mid := len(fileName) / 2 + fileName = fileName[mid-16 : mid+16] + } + return fileName + "." + imageFormat +} func ConvertStringRatingToType(rating string) (config.Rating, error) { switch rating { diff --git a/utils/transformers/tokens.go b/utils/transformers/tokens.go index 7ad36ed..f2f2e0b 100644 --- a/utils/transformers/tokens.go +++ b/utils/transformers/tokens.go @@ -1,6 +1,7 @@ package transformers import ( + "crypto" "crypto/rand" "encoding/hex" ) @@ -12,3 +13,9 @@ func GenerateRandomToken() (string, error) { } return hex.EncodeToString(bytes), nil } + +func GenerateTokenFromString(input string) string { + hasher := crypto.SHA256.New() + hasher.Write([]byte(input)) + return hex.EncodeToString(hasher.Sum(nil)) +} diff --git a/utils/validators/url.go b/utils/validators/url.go new file mode 100644 index 0000000..b85eca7 --- /dev/null +++ b/utils/validators/url.go @@ -0,0 +1,26 @@ +package validators + +import ( + "regexp" + "strings" +) + +func IsValidURL(url string) bool { + if url == "" { + return false + } + if len(url) > 2048 { // Common max URL length + return false + } + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + return false + } + + pattern := `^(http|https)://[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;=]+$` + matched, err := regexp.MatchString(pattern, url) + if err != nil { + return false + } + + return matched +} |
