aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-07-17 12:39:44 +0530
committerBobby <[email protected]>2025-07-17 12:39:44 +0530
commit46e41088bab946856867253c4fa268f9084b86a5 (patch)
tree617b5b31de5b2c0fe0cfd27f29b5580a784d468f
parentb0ba363696a758a8d0637107bd29a0a9ac1382d4 (diff)
downloadimageboard-46e41088bab946856867253c4fa268f9084b86a5.tar.xz
imageboard-46e41088bab946856867253c4fa268f9084b86a5.zip
webp support, decode and transformation functions
-rw-r--r--controllers/posts.go68
-rw-r--r--go.mod5
-rw-r--r--go.sum10
-rw-r--r--imageboard/main.go1
-rw-r--r--utils/format/files.go71
-rw-r--r--utils/format/format.go32
-rw-r--r--utils/format/numbers.go20
-rw-r--r--utils/format/time.go7
-rw-r--r--utils/transformers/image.go73
-rw-r--r--utils/transformers/tokens.go7
-rw-r--r--utils/validators/url.go26
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,
diff --git a/go.mod b/go.mod
index dc538e8..4027b9e 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 9799da1..205cefb 100644
--- a/go.sum
+++ b/go.sum
@@ -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
+}