re organizer project
This commit is contained in:
+14
-26
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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{}{})
|
||||||
|
}
|
||||||
@@ -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},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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{}{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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}
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user