Spaces:
Running
Running
upload handler
Browse files- backend/handlers/upload.go +20 -14
- backend/internal/storage/local.go +36 -0
- backend/internal/storage/s3.go +40 -0
- backend/internal/storage/storage.go +16 -0
- backend/main.go +64 -7
- backend/server/server.go +15 -0
backend/handlers/upload.go
CHANGED
|
@@ -6,20 +6,20 @@ import (
|
|
| 6 |
"database/sql"
|
| 7 |
"encoding/hex"
|
| 8 |
"io"
|
| 9 |
-
"mime/multipart"
|
| 10 |
"net/http"
|
| 11 |
"time"
|
| 12 |
|
| 13 |
"github.com/gin-gonic/gin"
|
| 14 |
"github.com/google/uuid"
|
| 15 |
-
"github.com/
|
| 16 |
-
|
| 17 |
)
|
| 18 |
|
|
|
|
| 19 |
// wire this in main.go: r.POST("/maps", deps.UploadMap)
|
| 20 |
type UploadDeps struct {
|
| 21 |
DB *sql.DB
|
| 22 |
-
Storage
|
| 23 |
Bucket string
|
| 24 |
RegionOK map[string]bool // in‑memory lookup, seeded at start
|
| 25 |
// same for SourceOK, CategoryOK, CountryOK
|
|
@@ -53,7 +53,12 @@ func (d *UploadDeps) UploadMap(c *gin.Context) {
|
|
| 53 |
// repeat for source / category / countries…
|
| 54 |
|
| 55 |
// ---- 3. Read file + hash it ----------------------------------------
|
| 56 |
-
buf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
sha := sha256.Sum256(buf)
|
| 58 |
shaHex := hex.EncodeToString(sha[:])
|
| 59 |
|
|
@@ -61,15 +66,16 @@ func (d *UploadDeps) UploadMap(c *gin.Context) {
|
|
| 61 |
objKey := "maps/" + time.Now().Format("2006/01/02/") + shaHex + ".png"
|
| 62 |
|
| 63 |
// ---- 4. Upload to object storage -----------------------------------
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
| 73 |
|
| 74 |
// ---- 5. Insert into maps -------------------------------------------
|
| 75 |
mapID := uuid.New()
|
|
|
|
| 6 |
"database/sql"
|
| 7 |
"encoding/hex"
|
| 8 |
"io"
|
|
|
|
| 9 |
"net/http"
|
| 10 |
"time"
|
| 11 |
|
| 12 |
"github.com/gin-gonic/gin"
|
| 13 |
"github.com/google/uuid"
|
| 14 |
+
"github.com/lib/pq"
|
| 15 |
+
"github.com/SCGR-1/promptaid-backend/internal/storage"
|
| 16 |
)
|
| 17 |
|
| 18 |
+
|
| 19 |
// wire this in main.go: r.POST("/maps", deps.UploadMap)
|
| 20 |
type UploadDeps struct {
|
| 21 |
DB *sql.DB
|
| 22 |
+
Storage storage.ObjectStore
|
| 23 |
Bucket string
|
| 24 |
RegionOK map[string]bool // in‑memory lookup, seeded at start
|
| 25 |
// same for SourceOK, CategoryOK, CountryOK
|
|
|
|
| 53 |
// repeat for source / category / countries…
|
| 54 |
|
| 55 |
// ---- 3. Read file + hash it ----------------------------------------
|
| 56 |
+
var buf []byte
|
| 57 |
+
buf, err = io.ReadAll(file)
|
| 58 |
+
if err != nil {
|
| 59 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "read failed"})
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
sha := sha256.Sum256(buf)
|
| 63 |
shaHex := hex.EncodeToString(sha[:])
|
| 64 |
|
|
|
|
| 66 |
objKey := "maps/" + time.Now().Format("2006/01/02/") + shaHex + ".png"
|
| 67 |
|
| 68 |
// ---- 4. Upload to object storage -----------------------------------
|
| 69 |
+
ctx := c.Request.Context()
|
| 70 |
+
if err := d.Storage.Put(
|
| 71 |
+
ctx, objKey,
|
| 72 |
+
bytes.NewReader(buf), int64(len(buf)),
|
| 73 |
+
fileHdr.Header.Get("Content-Type"),
|
| 74 |
+
); err != nil {
|
| 75 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "storage failed"})
|
| 76 |
+
return
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
|
| 80 |
// ---- 5. Insert into maps -------------------------------------------
|
| 81 |
mapID := uuid.New()
|
backend/internal/storage/local.go
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package storage
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"io"
|
| 6 |
+
"os"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
"time"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type LocalStore struct{ root string }
|
| 12 |
+
|
| 13 |
+
func NewLocalStore(root string) *LocalStore { return &LocalStore{root: root} }
|
| 14 |
+
|
| 15 |
+
func (l *LocalStore) Put(_ context.Context, key string, r io.Reader, _ int64, _ string) error {
|
| 16 |
+
full := filepath.Join(l.root, key)
|
| 17 |
+
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
| 18 |
+
return err
|
| 19 |
+
}
|
| 20 |
+
f, err := os.Create(full)
|
| 21 |
+
if err != nil {
|
| 22 |
+
return err
|
| 23 |
+
}
|
| 24 |
+
defer f.Close()
|
| 25 |
+
_, err = io.Copy(f, r)
|
| 26 |
+
return err
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func (l *LocalStore) Link(_ context.Context, key string, _ time.Duration) (string, error) {
|
| 30 |
+
// Served by Gin static handler: router.Static("/static", "./uploads")
|
| 31 |
+
return "/static/" + key, nil
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func (l *LocalStore) Root() string {
|
| 35 |
+
return l.root
|
| 36 |
+
}
|
backend/internal/storage/s3.go
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package storage
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"time"
|
| 6 |
+
"io"
|
| 7 |
+
|
| 8 |
+
"github.com/minio/minio-go/v7"
|
| 9 |
+
"github.com/minio/minio-go/v7/pkg/credentials"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type S3Store struct {
|
| 13 |
+
cli *minio.Client
|
| 14 |
+
bucket string
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
func NewS3Store(endpoint, accessKey, secretKey, bucket string, useSSL bool) (*S3Store, error) {
|
| 18 |
+
cli, err := minio.New(endpoint, &minio.Options{
|
| 19 |
+
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
| 20 |
+
Secure: useSSL,
|
| 21 |
+
})
|
| 22 |
+
if err != nil {
|
| 23 |
+
return nil, err
|
| 24 |
+
}
|
| 25 |
+
return &S3Store{cli: cli, bucket: bucket}, nil
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func (s *S3Store) Put(ctx context.Context, key string, r io.Reader, size int64, ctype string) error {
|
| 29 |
+
_, err := s.cli.PutObject(ctx, s.bucket, key, r, size,
|
| 30 |
+
minio.PutObjectOptions{ContentType: ctype})
|
| 31 |
+
return err
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func (s *S3Store) Link(ctx context.Context, key string, ttl time.Duration) (string, error) {
|
| 35 |
+
u, err := s.cli.PresignedGetObject(ctx, s.bucket, key, ttl, nil)
|
| 36 |
+
if err != nil {
|
| 37 |
+
return "", err
|
| 38 |
+
}
|
| 39 |
+
return u.String(), nil
|
| 40 |
+
}
|
backend/internal/storage/storage.go
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package storage
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"time"
|
| 6 |
+
"io"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
// ObjectStore is a minimal interface for saving and later linking to blobs.
|
| 10 |
+
type ObjectStore interface {
|
| 11 |
+
// Put permanently stores the object under key.
|
| 12 |
+
Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
|
| 13 |
+
|
| 14 |
+
// Link returns a URL valid for roughly ttl (ignored by LocalStore).
|
| 15 |
+
Link(ctx context.Context, key string, ttl time.Duration) (string, error)
|
| 16 |
+
}
|
backend/main.go
CHANGED
|
@@ -1,14 +1,71 @@
|
|
| 1 |
package main
|
| 2 |
|
| 3 |
import (
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
)
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
func main() {
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
|
|
|
| 1 |
package main
|
| 2 |
|
| 3 |
import (
|
| 4 |
+
"database/sql"
|
| 5 |
+
"log"
|
| 6 |
+
"os"
|
| 7 |
+
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
_ "github.com/lib/pq"
|
| 10 |
+
|
| 11 |
+
"github.com/SCGR-1/promptaid-backend/internal/storage"
|
| 12 |
+
"github.com/SCGR-1/promptaid-backend/handlers"
|
| 13 |
)
|
| 14 |
|
| 15 |
+
type Config struct {
|
| 16 |
+
S3Bucket string
|
| 17 |
+
UploadDir string
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func loadConfig() Config {
|
| 21 |
+
return Config{
|
| 22 |
+
S3Bucket: os.Getenv("S3_BUCKET"),
|
| 23 |
+
UploadDir: os.Getenv("UPLOAD_DIR"),
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
func main() {
|
| 29 |
+
|
| 30 |
+
cfg := loadConfig()
|
| 31 |
+
|
| 32 |
+
// ---- 1. connect DB ----
|
| 33 |
+
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
| 34 |
+
if err != nil { log.Fatal(err) }
|
| 35 |
+
|
| 36 |
+
// ---- 2. choose storage driver ----
|
| 37 |
+
var store storage.ObjectStore
|
| 38 |
+
switch os.Getenv("STORAGE_DRIVER") {
|
| 39 |
+
case "s3":
|
| 40 |
+
store, err = storage.NewS3Store(
|
| 41 |
+
os.Getenv("S3_ENDPOINT"),
|
| 42 |
+
os.Getenv("S3_KEY"),
|
| 43 |
+
os.Getenv("S3_SECRET"),
|
| 44 |
+
os.Getenv("S3_BUCKET"),
|
| 45 |
+
os.Getenv("S3_SSL") == "true",
|
| 46 |
+
)
|
| 47 |
+
if err != nil { log.Fatal(err) }
|
| 48 |
+
default: // local
|
| 49 |
+
uploadDir := os.Getenv("UPLOAD_DIR")
|
| 50 |
+
if uploadDir == "" { uploadDir = "./uploads" }
|
| 51 |
+
store = storage.NewLocalStore(uploadDir)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
uploadDeps := handlers.UploadDeps{
|
| 55 |
+
DB: db,
|
| 56 |
+
Storage: store,
|
| 57 |
+
Bucket: cfg.S3Bucket,
|
| 58 |
+
RegionOK: make(map[string]bool),
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// ---- 3. build server ----
|
| 62 |
+
r := gin.Default()
|
| 63 |
+
|
| 64 |
+
if l, ok := store.(*storage.LocalStore); ok {
|
| 65 |
+
r.Static("/static", l.Root()) // add Root() getter or hardcode "./uploads"
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
r.POST("/maps", uploadDeps.UploadMap)
|
| 69 |
+
|
| 70 |
+
log.Fatal(r.Run(":8080"))
|
| 71 |
}
|
backend/server/server.go
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package server //
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"database/sql"
|
| 5 |
+
"github.com/SCGR-1/promptaid-backend/internal/storage"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type Server struct {
|
| 9 |
+
db *sql.DB
|
| 10 |
+
store storage.ObjectStore
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
func NewServer(db *sql.DB, store storage.ObjectStore) *Server {
|
| 14 |
+
return &Server{db: db, store: store}
|
| 15 |
+
}
|