From 6b8c64bd406e32e0fb08562b1c6e48bb2ec9eeda Mon Sep 17 00:00:00 2001 From: m Date: Sun, 12 Jul 2026 13:44:15 +0200 Subject: [PATCH] re organizer project --- backend/cmd/server/main.go | 40 +-- backend/internal/config/config.go | 30 +++ backend/internal/handler/files.go | 130 ++++++++++ backend/internal/handler/handler.go | 19 ++ backend/internal/handler/health.go | 14 + backend/internal/handler/ocr.go | 22 ++ backend/internal/handler/router.go | 21 ++ backend/internal/handlers/files.go | 315 ----------------------- backend/internal/handlers/handlers.go | 11 - backend/internal/handlers/health.go | 15 -- backend/internal/handlers/ocr.go | 25 -- backend/internal/model/file.go | 18 ++ backend/internal/model/ocrjob.go | 10 + backend/internal/model/tag.go | 6 + backend/internal/models/models.go | 43 ---- backend/internal/service/checksum.go | 3 - backend/internal/service/file.go | 155 +++++++++++ backend/internal/service/ocr.go | 44 ++++ backend/internal/service/presignedurl.go | 41 --- backend/internal/service/url.go | 46 ++++ 20 files changed, 529 insertions(+), 479 deletions(-) create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/handler/files.go create mode 100644 backend/internal/handler/handler.go create mode 100644 backend/internal/handler/health.go create mode 100644 backend/internal/handler/ocr.go create mode 100644 backend/internal/handler/router.go delete mode 100644 backend/internal/handlers/files.go delete mode 100644 backend/internal/handlers/handlers.go delete mode 100644 backend/internal/handlers/health.go delete mode 100644 backend/internal/handlers/ocr.go create mode 100644 backend/internal/model/file.go create mode 100644 backend/internal/model/ocrjob.go create mode 100644 backend/internal/model/tag.go delete mode 100644 backend/internal/models/models.go create mode 100644 backend/internal/service/file.go create mode 100644 backend/internal/service/ocr.go delete mode 100644 backend/internal/service/presignedurl.go create mode 100644 backend/internal/service/url.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 3d214c7..974155d 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -2,20 +2,16 @@ package main import ( "log" - "os" "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/internal/config" "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() { - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } - - migrationsPath := "file://internal/db/migrations" + cfg := config.Load() database, err := db.Connect() if err != nil { @@ -23,32 +19,24 @@ func main() { } defer database.Close() + migrationsPath := "file://internal/db/migrations" if err := db.RunMigrations(migrationsPath); err != nil { log.Fatalf("Failed to run migrations: %v", err) } 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() + handler.SetupRoutes(r, h) - r.GET("/api/v1/health", h.Health) - - 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.Printf("Server starting on port %s", cfg.Port) + if err := r.Run(":" + cfg.Port); err != nil { log.Fatal(err) } } diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..3adb62e --- /dev/null +++ b/backend/internal/config/config.go @@ -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 +} diff --git a/backend/internal/handler/files.go b/backend/internal/handler/files.go new file mode 100644 index 0000000..5a56046 --- /dev/null +++ b/backend/internal/handler/files.go @@ -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{}{}) +} diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go new file mode 100644 index 0000000..b154863 --- /dev/null +++ b/backend/internal/handler/handler.go @@ -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}, + } +} diff --git a/backend/internal/handler/health.go b/backend/internal/handler/health.go new file mode 100644 index 0000000..44f5463 --- /dev/null +++ b/backend/internal/handler/health.go @@ -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"}) +} diff --git a/backend/internal/handler/ocr.go b/backend/internal/handler/ocr.go new file mode 100644 index 0000000..cb3938a --- /dev/null +++ b/backend/internal/handler/ocr.go @@ -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") +} diff --git a/backend/internal/handler/router.go b/backend/internal/handler/router.go new file mode 100644 index 0000000..487fe2d --- /dev/null +++ b/backend/internal/handler/router.go @@ -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) +} diff --git a/backend/internal/handlers/files.go b/backend/internal/handlers/files.go deleted file mode 100644 index 60defb5..0000000 --- a/backend/internal/handlers/files.go +++ /dev/null @@ -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{}{}, - }) -} diff --git a/backend/internal/handlers/handlers.go b/backend/internal/handlers/handlers.go deleted file mode 100644 index 7216b92..0000000 --- a/backend/internal/handlers/handlers.go +++ /dev/null @@ -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} -} diff --git a/backend/internal/handlers/health.go b/backend/internal/handlers/health.go deleted file mode 100644 index e1313e3..0000000 --- a/backend/internal/handlers/health.go +++ /dev/null @@ -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", - }, - }) -} diff --git a/backend/internal/handlers/ocr.go b/backend/internal/handlers/ocr.go deleted file mode 100644 index 1f02b11..0000000 --- a/backend/internal/handlers/ocr.go +++ /dev/null @@ -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", - }, - }) -} diff --git a/backend/internal/model/file.go b/backend/internal/model/file.go new file mode 100644 index 0000000..d258407 --- /dev/null +++ b/backend/internal/model/file.go @@ -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"` +} diff --git a/backend/internal/model/ocrjob.go b/backend/internal/model/ocrjob.go new file mode 100644 index 0000000..435a6a0 --- /dev/null +++ b/backend/internal/model/ocrjob.go @@ -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"` +} diff --git a/backend/internal/model/tag.go b/backend/internal/model/tag.go new file mode 100644 index 0000000..06b0c9a --- /dev/null +++ b/backend/internal/model/tag.go @@ -0,0 +1,6 @@ +package model + +type Tag struct { + ID string `json:"id"` + Name string `json:"name"` +} diff --git a/backend/internal/models/models.go b/backend/internal/models/models.go deleted file mode 100644 index 01488c0..0000000 --- a/backend/internal/models/models.go +++ /dev/null @@ -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"` -} diff --git a/backend/internal/service/checksum.go b/backend/internal/service/checksum.go index f2c2b3a..e15f508 100644 --- a/backend/internal/service/checksum.go +++ b/backend/internal/service/checksum.go @@ -7,9 +7,7 @@ import ( func CreateSHA256Hash(data []byte) []byte { hasher := sha256.New() - hasher.Write(data) - return hasher.Sum(nil) } @@ -17,6 +15,5 @@ func CompareHash(x, y []byte) bool { if len(x) != len(y) { return false } - return subtle.ConstantTimeCompare(x, y) == 1 } diff --git a/backend/internal/service/file.go b/backend/internal/service/file.go new file mode 100644 index 0000000..3ac66f2 --- /dev/null +++ b/backend/internal/service/file.go @@ -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 +} diff --git a/backend/internal/service/ocr.go b/backend/internal/service/ocr.go new file mode 100644 index 0000000..33145e8 --- /dev/null +++ b/backend/internal/service/ocr.go @@ -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() +} diff --git a/backend/internal/service/presignedurl.go b/backend/internal/service/presignedurl.go deleted file mode 100644 index b14fd6c..0000000 --- a/backend/internal/service/presignedurl.go +++ /dev/null @@ -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)) -} diff --git a/backend/internal/service/url.go b/backend/internal/service/url.go new file mode 100644 index 0000000..37b5d32 --- /dev/null +++ b/backend/internal/service/url.go @@ -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)) +}