re organizer project
This commit is contained in:
@@ -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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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