feat(storage): add S3 backend support and advanced upload limits
- Introduce S3-compatible storage backend support using minio-go. - Add configuration options for local storage limits, box limits, and rate limiting. - Implement storage backend selection (local vs S3) for anonymous and registered users. - Add an `/admin/storage` management interface. - Update documentation and environment examples with the new configuration variables.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -129,43 +132,71 @@ func needsThumbnail(file services.File) bool {
|
||||
|
||||
func generateThumbnail(uploadService *services.UploadService, box services.Box, file services.File) (string, error) {
|
||||
thumbnailName := "@thumb@" + file.ID + ".jpg"
|
||||
thumbnailPath := uploadService.ThumbnailPath(box, services.File{Thumbnail: thumbnailName})
|
||||
sourcePath := uploadService.FilePath(box, file)
|
||||
object, err := uploadService.OpenFileObject(context.Background(), box, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer object.Body.Close()
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(file.ContentType, "image/"):
|
||||
return thumbnailName, createImageThumbnail(sourcePath, thumbnailPath)
|
||||
data, err := createImageThumbnail(object.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = uploadService.PutThumbnailObject(context.Background(), box, thumbnailName, bytes.NewReader(data), int64(len(data)), "image/jpeg")
|
||||
return thumbnailName, err
|
||||
case strings.HasPrefix(file.ContentType, "video/"):
|
||||
return thumbnailName, createVideoThumbnail(sourcePath, thumbnailPath)
|
||||
data, err := createVideoThumbnail(object.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = uploadService.PutThumbnailObject(context.Background(), box, thumbnailName, bytes.NewReader(data), int64(len(data)), "image/jpeg")
|
||||
return thumbnailName, err
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func createImageThumbnail(sourcePath, targetPath string) error {
|
||||
source, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
func createImageThumbnail(source io.Reader) ([]byte, error) {
|
||||
img, _, err := image.Decode(source)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thumb := resizeNearest(img, 360, 240)
|
||||
target, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
||||
var target bytes.Buffer
|
||||
err = jpeg.Encode(&target, thumb, &jpeg.Options{Quality: 82})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer target.Close()
|
||||
|
||||
return jpeg.Encode(target, thumb, &jpeg.Options{Quality: 82})
|
||||
return target.Bytes(), nil
|
||||
}
|
||||
|
||||
func createVideoThumbnail(sourcePath, targetPath string) error {
|
||||
return exec.Command("ffmpeg", "-y", "-loglevel", "error", "-ss", "00:00:01", "-i", sourcePath, "-frames:v", "1", "-vf", "scale=360:-1", targetPath).Run()
|
||||
func createVideoThumbnail(source io.Reader) ([]byte, error) {
|
||||
sourceFile, err := os.CreateTemp("", "warpbox-video-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(sourceFile.Name())
|
||||
if _, err := io.Copy(sourceFile, source); err != nil {
|
||||
sourceFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := sourceFile.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetFile, err := os.CreateTemp("", "warpbox-thumb-*.jpg")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetPath := targetFile.Name()
|
||||
targetFile.Close()
|
||||
defer os.Remove(targetPath)
|
||||
if err := exec.Command("ffmpeg", "-y", "-loglevel", "error", "-ss", "00:00:01", "-i", sourceFile.Name(), "-frames:v", "1", "-vf", "scale=360:-1", targetPath).Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(targetPath)
|
||||
}
|
||||
|
||||
func resizeNearest(src image.Image, maxWidth, maxHeight int) *image.RGBA {
|
||||
|
||||
Reference in New Issue
Block a user