re organizer project

This commit is contained in:
m
2026-07-12 13:44:15 +02:00
parent 999fbaf7fa
commit 6b8c64bd40
20 changed files with 529 additions and 479 deletions
+14 -26
View File
@@ -2,20 +2,16 @@ package main
import ( import (
"log" "log"
"os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/config"
"github.com/vaultdrop/backend/internal/db" "github.com/vaultdrop/backend/internal/db"
"github.com/vaultdrop/backend/internal/handlers" "github.com/vaultdrop/backend/internal/handler"
"github.com/vaultdrop/backend/internal/service"
) )
func main() { func main() {
port := os.Getenv("PORT") cfg := config.Load()
if port == "" {
port = "8080"
}
migrationsPath := "file://internal/db/migrations"
database, err := db.Connect() database, err := db.Connect()
if err != nil { if err != nil {
@@ -23,32 +19,24 @@ func main() {
} }
defer database.Close() defer database.Close()
migrationsPath := "file://internal/db/migrations"
if err := db.RunMigrations(migrationsPath); err != nil { if err := db.RunMigrations(migrationsPath); err != nil {
log.Fatalf("Failed to run migrations: %v", err) log.Fatalf("Failed to run migrations: %v", err)
} }
queries := db.New(database) queries := db.New(database)
h := handlers.New(queries)
fileSvc := service.NewFileService(queries, cfg)
ocrSvc := service.NewOCRService(cfg)
urlSvc := service.NewURLService(cfg.HMACSecret, cfg.ServerHost)
h := handler.New(fileSvc, ocrSvc, urlSvc)
r := gin.Default() r := gin.Default()
handler.SetupRoutes(r, h)
r.GET("/api/v1/health", h.Health) log.Printf("Server starting on port %s", cfg.Port)
if err := r.Run(":" + cfg.Port); err != nil {
r.GET("/api/v1/files", h.ListFiles)
r.GET("/api/v1/file/:id", h.ListFile)
r.POST("/api/v1/files/upload", h.UploadFiles)
r.GET("/api/v1/files/:id", h.GetFile)
r.DELETE("/api/v1/files/:id", h.DeleteFile)
r.GET("/api/v1/files/search", h.SearchFiles)
r.POST("/api/v1/files/:id/tags", h.AddTags)
r.GET("/api/v1/files/:id/tags", h.GetTags)
r.POST("/api/v1/ocr/jobs", h.CreateOcrJob)
r.GET("/api/v1/ocr/jobs/:id", h.GetOcrJobStatus)
log.Printf("Server starting on port %s", port)
if err := r.Run(":" + port); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
+30
View File
@@ -0,0 +1,30 @@
package config
import "os"
type Config struct {
Port string
DBPath string
OCREndpoint string
UploadDir string
HMACSecret string
ServerHost string
}
func Load() *Config {
return &Config{
Port: envOr("PORT", "8080"),
DBPath: envOr("DB_PATH", "vaultdrop.db"),
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
UploadDir: envOr("UPLOAD_DIR", "./uploads"),
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
ServerHost: envOr("SERVER_HOST", "http://localhost:8080"),
}
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
+130
View File
@@ -0,0 +1,130 @@
package handler
import (
"net/http"
"path"
"strconv"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/service"
"github.com/vaultdrop/backend/pkg/api"
)
type FileHandler struct {
files *service.FileService
urls *service.URLService
}
func (h *FileHandler) Upload(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
api.Error(c, http.StatusBadRequest, "ERROR_PARSING", "Error while parsing multipart form")
return
}
files := form.File["file"]
if len(files) == 0 {
api.Error(c, http.StatusBadRequest, "NO_FILES", "No files provided")
return
}
results := make([]gin.H, 0, len(files))
for _, file := range files {
result, err := h.files.Upload(file)
if err != nil {
api.Error(c, http.StatusInternalServerError, "UPLOAD_ERROR", err.Error())
return
}
results = append(results, gin.H{
"id": result.ID,
"name": result.Name,
})
}
api.Success(c, results)
}
func (h *FileHandler) List(c *gin.Context) {
files, err := h.files.List()
if err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files")
return
}
for i := range files {
files[i].CreatedAt = "" // clean for response
}
type fileResponse struct {
ID string `json:"id"`
URL string `json:"url"`
Name string `json:"name"`
Size int64 `json:"size"`
Tags []string `json:"tags"`
}
resp := make([]fileResponse, len(files))
for i, f := range files {
resp[i] = fileResponse{
ID: f.ID,
URL: h.urls.GenerateDownloadURL(f.ID),
Name: f.Name,
Size: f.Size,
Tags: []string{},
}
}
api.Paginated(c, resp, 1, len(resp))
}
func (h *FileHandler) Download(c *gin.Context) {
id := c.Param("id")
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
sig := c.Query("sig")
if !h.urls.Validate(id, sig, exp) {
api.Error(c, http.StatusForbidden, "FORBIDDEN", "Invalid or expired link")
return
}
storagePath, err := h.files.GetStoragePath(id)
if err != nil {
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
return
}
c.File(path.Clean(storagePath))
}
func (h *FileHandler) Get(c *gin.Context) {
id := c.Param("id")
file, err := h.files.Get(id)
if err != nil {
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
return
}
api.Success(c, gin.H{
"id": file.ID,
"name": file.Name,
"url": h.urls.GenerateDownloadURL(file.ID),
"size": file.Size,
})
}
func (h *FileHandler) Delete(c *gin.Context) {
id := c.Param("id")
if err := h.files.Delete(id); err != nil {
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete file")
return
}
api.Success(c, gin.H{"deleted": true})
}
func (h *FileHandler) AddTags(c *gin.Context) {
api.Error(c, http.StatusNotImplemented, "NOT_IMPLEMENTED", "Tags not yet implemented")
}
func (h *FileHandler) GetTags(c *gin.Context) {
api.Success(c, []interface{}{})
}
+19
View File
@@ -0,0 +1,19 @@
package handler
import (
"github.com/vaultdrop/backend/internal/service"
)
type Handler struct {
File *FileHandler
OCR *OCRHandler
Health *HealthHandler
}
func New(fileSvc *service.FileService, ocrSvc *service.OCRService, urlSvc *service.URLService) *Handler {
return &Handler{
File: &FileHandler{files: fileSvc, urls: urlSvc},
OCR: &OCRHandler{ocr: ocrSvc, files: fileSvc},
Health: &HealthHandler{ocr: ocrSvc},
}
}
+14
View File
@@ -0,0 +1,14 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/pkg/api"
)
type HealthHandler struct {
ocr interface{ HealthCheck() error }
}
func (h *HealthHandler) Check(c *gin.Context) {
api.Success(c, gin.H{"status": "healthy"})
}
+22
View File
@@ -0,0 +1,22 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/service"
"github.com/vaultdrop/backend/pkg/api"
)
type OCRHandler struct {
ocr *service.OCRService
files *service.FileService
}
func (h *OCRHandler) CreateJob(c *gin.Context) {
api.Error(c, http.StatusNotImplemented, "NOT_IMPLEMENTED", "OCR job creation not yet implemented")
}
func (h *OCRHandler) GetJobStatus(c *gin.Context) {
api.Error(c, http.StatusNotFound, "JOB_NOT_FOUND", "OCR job not found")
}
+21
View File
@@ -0,0 +1,21 @@
package handler
import "github.com/gin-gonic/gin"
func SetupRoutes(r *gin.Engine, h *Handler) {
api := r.Group("/api/v1")
api.GET("/health", h.Health.Check)
api.GET("/files", h.File.List)
api.POST("/files/upload", h.File.Upload)
api.GET("/files/download/:id", h.File.Download)
api.GET("/files/:id", h.File.Get)
api.DELETE("/files/:id", h.File.Delete)
api.POST("/files/:id/tags", h.File.AddTags)
api.GET("/files/:id/tags", h.File.GetTags)
api.POST("/ocr/jobs", h.OCR.CreateJob)
api.GET("/ocr/jobs/:id", h.OCR.GetJobStatus)
}
-315
View File
@@ -1,315 +0,0 @@
package handlers
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"github.com/vaultdrop/backend/internal/db"
"github.com/vaultdrop/backend/internal/ocr"
"github.com/vaultdrop/backend/internal/service"
)
// retourne qui est delete qui est updated et created
func (h *Handlers) SyncFiles(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "Sync not yet implemented",
},
})
}
func (h *Handlers) ListFiles(c *gin.Context) {
dirs, err := os.ReadDir("./uploads/")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"data": []interface{}{},
"meta": gin.H{
"page": 1,
"total": 0,
},
})
return
}
type Finfo struct {
Url string `json:"url"`
Name string `json:"name"`
Size int64 `json:"size"`
Tags []string `json:"tags"`
}
files := []Finfo{}
for _, dir := range dirs {
if dir.IsDir() {
continue
}
i, e := dir.Info()
if e != nil {
continue
}
ps := Finfo{
Url: service.GenerateFileDownloadUrl(i.Name()),
Name: i.Name(),
Size: i.Size(),
Tags: []string{},
}
files = append(files, ps)
}
c.JSON(http.StatusOK, gin.H{
"data": files,
"meta": gin.H{
"page": 1,
"total": 0,
},
})
}
func (h *Handlers) ListFile(c *gin.Context) {
id := c.Param("id")
dirs, err := os.ReadDir("./uploads/")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"data": []interface{}{},
"meta": gin.H{
"page": 1,
"total": 0,
},
})
return
}
type Finfo struct {
Url string `json:"url"`
Name string `json:"name"`
Size int64 `json:"size"`
Tags []string `json:"tags"`
}
file := Finfo{}
for _, dir := range dirs {
if dir.IsDir() {
continue
}
i, e := dir.Info()
if e != nil {
continue
}
if id != i.Name() {
continue
}
ps := Finfo{
Url: service.GenerateFileDownloadUrl(i.Name()),
Name: i.Name(),
Size: i.Size(),
Tags: []string{},
}
file = ps
break
}
c.JSON(http.StatusOK, gin.H{
"data": file,
"meta": gin.H{
"total": 1,
},
})
}
func (h *Handlers) GetFile(c *gin.Context) {
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
r := service.Validate(c.Params.ByName("id"), c.Query("sig"), exp)
if r != true {
c.JSON(http.StatusForbidden, gin.H{
"error": gin.H{
"code": "FILE_NOT_FOUND",
"message": "File not found",
},
})
return
}
c.File(path.Join("./uploads/", c.Params.ByName("id")))
}
func (h *Handlers) UploadFiles(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": gin.H{
"code": "ERROR_PARSING",
"message": "Error while parsing multipart form",
},
})
return
}
files := form.File["file"]
type FileStats struct {
Name string `json:"name"`
Id string `json:"id"`
}
client := ocr.NewClient("http://localhost:9090")
filesStats := []FileStats{}
for _, file := range files {
dst := filepath.Join("./uploads/", filepath.Base(file.Filename))
err = c.SaveUploadedFile(file, dst)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "ERROR",
"message": "Uploaded",
},
})
return
}
fileByte, err := os.ReadFile(dst)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "ERROR",
"message": "Error reading file",
},
})
return
}
checksum := service.CreateSHA256Hash(fileByte)
ctx := context.Background()
id := make([]byte, 16)
rand.Read(id)
createFileParams := db.CreateFileParams{
ID: hex.EncodeToString(id),
Name: file.Filename,
Size: file.Size,
StorageKey: dst,
Checksum: hex.EncodeToString(checksum),
}
dbFile, err := h.queries.CreateFile(ctx, createFileParams)
if err != nil {
fmt.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"code": "DB_ERROR",
"message": "Failed to save file metadata",
},
})
return
}
filesStats = append(filesStats, FileStats{
Name: dbFile.Name,
Id: dbFile.ID,
})
text, err := client.Recognize(fileByte)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(text)
}
c.JSON(http.StatusOK, gin.H{
"data": filesStats,
})
}
func (h *Handlers) DeleteFile(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "Delete not yet implemented",
},
})
}
func (h *Handlers) SearchFiles(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []interface{}{},
"meta": gin.H{
"page": 1,
"total": 0,
},
})
}
func (h *Handlers) AddTags(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "Add tags not yet implemented",
},
})
}
func (h *Handlers) GetTags(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []interface{}{},
})
}
-11
View File
@@ -1,11 +0,0 @@
package handlers
import "github.com/vaultdrop/backend/internal/db"
type Handlers struct {
queries *db.Queries
}
func New(queries *db.Queries) *Handlers {
return &Handlers{queries: queries}
}
-15
View File
@@ -1,15 +0,0 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func (h *Handlers) Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"status": "healthy",
},
})
}
-25
View File
@@ -1,25 +0,0 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func (h *Handlers) CreateOcrJob(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{
"error": gin.H{
"code": "NOT_IMPLEMENTED",
"message": "OCR job creation not yet implemented",
},
})
}
func (h *Handlers) GetOcrJobStatus(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": gin.H{
"code": "JOB_NOT_FOUND",
"message": "OCR job not found",
},
})
}
+18
View File
@@ -0,0 +1,18 @@
package model
type File struct {
ID string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
StorageKey string `json:"-"`
Checksum string `json:"-"`
OcrText string `json:"ocrText,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type UploadResult struct {
ID string `json:"id"`
Name string `json:"name"`
}
+10
View File
@@ -0,0 +1,10 @@
package model
type OcrJob struct {
ID string `json:"id"`
FileID string `json:"fileId"`
Status string `json:"status"`
Result string `json:"result,omitempty"`
CreatedAt string `json:"createdAt"`
CompletedAt *string `json:"completedAt,omitempty"`
}
+6
View File
@@ -0,0 +1,6 @@
package model
type Tag struct {
ID string `json:"id"`
Name string `json:"name"`
}
-43
View File
@@ -1,43 +0,0 @@
package models
import "time"
type File struct {
ID string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
Size int64 `json:"size"`
Path string `json:"-"`
OcrText string `json:"ocrText,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Tag struct {
ID string `json:"id"`
Name string `json:"name"`
}
type OcrJob struct {
ID string `json:"id"`
FileID string `json:"fileId"`
Status string `json:"status"`
Result string `json:"result,omitempty"`
CreatedAt time.Time `json:"createdAt"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Meta struct {
Page int `json:"page"`
Total int `json:"total"`
} `json:"meta"`
}
type ErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
-3
View File
@@ -7,9 +7,7 @@ import (
func CreateSHA256Hash(data []byte) []byte { func CreateSHA256Hash(data []byte) []byte {
hasher := sha256.New() hasher := sha256.New()
hasher.Write(data) hasher.Write(data)
return hasher.Sum(nil) return hasher.Sum(nil)
} }
@@ -17,6 +15,5 @@ func CompareHash(x, y []byte) bool {
if len(x) != len(y) { if len(x) != len(y) {
return false return false
} }
return subtle.ConstantTimeCompare(x, y) == 1 return subtle.ConstantTimeCompare(x, y) == 1
} }
+155
View File
@@ -0,0 +1,155 @@
package service
import (
"context"
"encoding/hex"
"fmt"
"mime/multipart"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/vaultdrop/backend/internal/config"
"github.com/vaultdrop/backend/internal/db"
"github.com/vaultdrop/backend/internal/model"
)
type FileService struct {
queries *db.Queries
cfg *config.Config
}
func NewFileService(queries *db.Queries, cfg *config.Config) *FileService {
return &FileService{queries: queries, cfg: cfg}
}
func (s *FileService) Upload(file *multipart.FileHeader) (*model.UploadResult, error) {
dst := filepath.Join(s.cfg.UploadDir, uuid.New().String()+filepath.Ext(file.Filename))
if err := os.MkdirAll(s.cfg.UploadDir, 0o755); err != nil {
return nil, fmt.Errorf("create upload dir: %w", err)
}
if err := saveUploadedFile(file, dst); err != nil {
return nil, fmt.Errorf("save file: %w", err)
}
data, err := os.ReadFile(dst)
if err != nil {
return nil, fmt.Errorf("read saved file: %w", err)
}
info, err := os.Stat(dst)
if err != nil {
return nil, fmt.Errorf("stat file: %w", err)
}
id := uuid.New().String()
checksum := hex.EncodeToString(CreateSHA256Hash(data))
dbFile, err := s.queries.CreateFile(context.Background(), db.CreateFileParams{
ID: id,
Name: file.Filename,
MimeType: file.Header.Get("Content-Type"),
Size: info.Size(),
StorageKey: dst,
Checksum: checksum,
})
if err != nil {
return nil, fmt.Errorf("create file in db: %w", err)
}
return &model.UploadResult{
ID: dbFile.ID,
Name: dbFile.Name,
}, nil
}
func (s *FileService) List() ([]model.File, error) {
dbFiles, err := s.queries.ListFiles(context.Background())
if err != nil {
return nil, fmt.Errorf("list files: %w", err)
}
files := make([]model.File, len(dbFiles))
for i, f := range dbFiles {
files[i] = dbToModel(f)
}
return files, nil
}
func (s *FileService) Get(id string) (*model.File, error) {
f, err := s.queries.GetFile(context.Background(), id)
if err != nil {
return nil, fmt.Errorf("get file: %w", err)
}
m := dbToModel(f)
return &m, nil
}
func (s *FileService) Delete(id string) error {
return s.queries.DeleteFile(context.Background(), id)
}
func (s *FileService) GetStoragePath(id string) (string, error) {
f, err := s.queries.GetFile(context.Background(), id)
if err != nil {
return "", fmt.Errorf("get file: %w", err)
}
return f.StorageKey, nil
}
func (s *FileService) UpdateOCRText(id, text string) error {
f, err := s.queries.GetFile(context.Background(), id)
if err != nil {
return fmt.Errorf("get file: %w", err)
}
return s.queries.UpdateFile(context.Background(), db.UpdateFileParams{
Name: f.Name,
MimeType: f.MimeType,
OcrText: text,
ID: id,
})
}
func dbToModel(f db.File) model.File {
return model.File{
ID: f.ID,
Name: f.Name,
MimeType: f.MimeType,
Size: f.Size,
StorageKey: f.StorageKey,
Checksum: f.Checksum,
OcrText: f.OcrText,
CreatedAt: f.CreatedAt.String(),
UpdatedAt: f.UpdatedAt.String(),
}
}
func saveUploadedFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
buf := make([]byte, 32*1024)
for {
n, readErr := src.Read(buf)
if n > 0 {
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
return writeErr
}
}
if readErr != nil {
break
}
}
return nil
}
+44
View File
@@ -0,0 +1,44 @@
package service
import (
"fmt"
"os"
"strings"
"github.com/vaultdrop/backend/internal/config"
"github.com/vaultdrop/backend/internal/ocr"
)
type OCRService struct {
client *ocr.Client
}
func NewOCRService(cfg *config.Config) *OCRService {
return &OCRService{
client: ocr.NewClient(cfg.OCREndpoint),
}
}
func (s *OCRService) RecognizeFromFile(filePath string) ([]ocr.TextBlock, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
return s.client.Recognize(data)
}
func (s *OCRService) RecognizeFromBytes(data []byte) ([]ocr.TextBlock, error) {
return s.client.Recognize(data)
}
func (s *OCRService) FlattenResults(blocks []ocr.TextBlock) string {
var texts []string
for _, b := range blocks {
texts = append(texts, b.Text)
}
return strings.Join(texts, "\n")
}
func (s *OCRService) HealthCheck() error {
return s.client.HealthCheck()
}
-41
View File
@@ -1,41 +0,0 @@
package service
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
const secret = "thisismyrandomstring"
func sign(fileID string, expires int64, secret string) string {
data := fmt.Sprintf("%s:%d", fileID, expires)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
func GenerateFileDownloadUrl(fileID string) string {
expires := time.Now().Add(10 * time.Minute).Unix()
sig := sign(fileID, expires, secret)
url := fmt.Sprintf(
"http://192.168.1.17:8080/api/v1/files/%s?expires=%d&sig=%s",
fileID,
expires,
sig,
)
return url
}
func Validate(fileID, sig string, expires int64) bool {
if time.Now().Unix() > expires {
return false
}
expected := sign(fileID, expires, secret)
return hmac.Equal([]byte(sig), []byte(expected))
}
+46
View File
@@ -0,0 +1,46 @@
package service
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
)
type URLService struct {
secret string
serverHost string
}
func NewURLService(secret, serverHost string) *URLService {
return &URLService{secret: secret, serverHost: serverHost}
}
func (s *URLService) sign(fileID string, expires int64) string {
data := fmt.Sprintf("%s:%d", fileID, expires)
mac := hmac.New(sha256.New, []byte(s.secret))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
func (s *URLService) GenerateDownloadURL(fileUUID string) string {
expires := time.Now().Add(10 * time.Minute).Unix()
sig := s.sign(fileUUID, expires)
return fmt.Sprintf(
"%s/api/v1/files/%s?expires=%d&sig=%s",
s.serverHost,
fileUUID,
expires,
sig,
)
}
func (s *URLService) Validate(fileID, sig string, expires int64) bool {
if time.Now().Unix() > expires {
return false
}
expected := s.sign(fileID, expires)
return hmac.Equal([]byte(sig), []byte(expected))
}