create tests directory format
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
# Binaries
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output
|
|
||||||
*.out
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
vendor/
|
|
||||||
|
|
||||||
# Uploads
|
|
||||||
uploads/*
|
|
||||||
!uploads/.gitkeep
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# Database
|
|
||||||
*.db
|
|
||||||
*.db-wal
|
|
||||||
*.db-shm
|
|
||||||
|
|
||||||
# Legacy SQLite (kept for safety)
|
|
||||||
vaultdrop.db
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
FROM golang:1.24-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/server ./cmd/server
|
|
||||||
|
|
||||||
FROM alpine:3.21
|
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates curl libreoffice-core poppler-utils
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /bin/server .
|
|
||||||
COPY internal/db/migrations ./internal/db/migrations
|
|
||||||
|
|
||||||
RUN mkdir -p /app/uploads /app/uploads/thumbnails /data
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD ["./server"]
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
_ = godotenv.Load()
|
|
||||||
|
|
||||||
cfg := config.Load()
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", cfg.DatabaseURL)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to database: %v", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
uploadDir := cfg.UploadDir
|
|
||||||
thumbnailDir := cfg.ThumbnailDir
|
|
||||||
|
|
||||||
fmt.Println("=== VaultDrop Orphan GC ===")
|
|
||||||
fmt.Printf("Upload dir: %s\n", uploadDir)
|
|
||||||
fmt.Printf("Thumbnail dir: %s\n", thumbnailDir)
|
|
||||||
|
|
||||||
var fileCount int
|
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM files WHERE is_folder = false AND storage_key != ''").Scan(&fileCount)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to count files: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Files in DB: %d\n", fileCount)
|
|
||||||
|
|
||||||
rows, err := db.Query("SELECT id, storage_key FROM files WHERE is_folder = false AND storage_key != ''")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to query files: %v", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
dbPaths := make(map[string]string)
|
|
||||||
for rows.Next() {
|
|
||||||
var id, storageKey string
|
|
||||||
if err := rows.Scan(&id, &storageKey); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dbPaths[storageKey] = id
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(uploadDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to read upload dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
orphanFiles := 0
|
|
||||||
freedBytes := int64(0)
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fullPath := filepath.Join(uploadDir, entry.Name())
|
|
||||||
relPath := "./" + fullPath
|
|
||||||
|
|
||||||
if _, ok := dbPaths[fullPath]; !ok {
|
|
||||||
if _, ok2 := dbPaths[relPath]; !ok2 {
|
|
||||||
info, err := entry.Info()
|
|
||||||
if err == nil {
|
|
||||||
freedBytes += info.Size()
|
|
||||||
}
|
|
||||||
orphanFiles++
|
|
||||||
fmt.Printf(" ORPHAN FILE: %s\n", fullPath)
|
|
||||||
os.Remove(fullPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var thumbCount int
|
|
||||||
err = db.QueryRow("SELECT COUNT(*) FROM thumbnails").Scan(&thumbCount)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to count thumbnails: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("Thumbnails in DB: %d\n", thumbCount)
|
|
||||||
|
|
||||||
thumbRows, err := db.Query("SELECT id, file_id, storage_key FROM thumbnails")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to query thumbnails: %v", err)
|
|
||||||
}
|
|
||||||
defer thumbRows.Close()
|
|
||||||
|
|
||||||
dbThumbPaths := make(map[string]string)
|
|
||||||
for thumbRows.Next() {
|
|
||||||
var id, fileID, storageKey string
|
|
||||||
if err := thumbRows.Scan(&id, &fileID, &storageKey); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dbThumbPaths[storageKey] = fileID
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(thumbnailDir); err == nil {
|
|
||||||
fileDirs, err := os.ReadDir(thumbnailDir)
|
|
||||||
if err == nil {
|
|
||||||
for _, fileDir := range fileDirs {
|
|
||||||
if !fileDir.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fileDirPath := filepath.Join(thumbnailDir, fileDir.Name())
|
|
||||||
thumbFiles, err := os.ReadDir(fileDirPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, tf := range thumbFiles {
|
|
||||||
if tf.IsDir() || strings.HasPrefix(tf.Name(), ".") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
thumbPath := filepath.Join(fileDirPath, tf.Name())
|
|
||||||
relThumbPath := "./" + thumbPath
|
|
||||||
if _, ok := dbThumbPaths[thumbPath]; !ok {
|
|
||||||
if _, ok2 := dbThumbPaths[relThumbPath]; !ok2 {
|
|
||||||
info, err := tf.Info()
|
|
||||||
if err == nil {
|
|
||||||
freedBytes += info.Size()
|
|
||||||
}
|
|
||||||
orphanFiles++
|
|
||||||
fmt.Printf(" ORPHAN THUMB: %s\n", thumbPath)
|
|
||||||
os.Remove(thumbPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining, _ := os.ReadDir(fileDirPath)
|
|
||||||
if len(remaining) == 0 {
|
|
||||||
os.Remove(fileDirPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n=== Summary ===\n")
|
|
||||||
fmt.Printf("Orphan files removed: %d\n", orphanFiles)
|
|
||||||
fmt.Printf("Space freed: %.2f MB\n", float64(freedBytes)/(1024*1024))
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"github.com/vaultdrop/backend/internal/auth"
|
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
"github.com/vaultdrop/backend/internal/handler"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
_ = godotenv.Load()
|
|
||||||
|
|
||||||
cfg := config.Load()
|
|
||||||
|
|
||||||
database, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to database: %v", err)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
eventBroker := service.NewEventBroker()
|
|
||||||
|
|
||||||
resourceSvc := service.NewResourceService(database, queries, cfg)
|
|
||||||
ocrSvc := service.NewOCRService(database, queries, cfg, resourceSvc, eventBroker)
|
|
||||||
conversionSvc := service.NewConversionService(queries, cfg)
|
|
||||||
urlSvc := service.NewURLService(cfg.HMACSecret, cfg.ServerHost, cfg.URLExpiryMinutes)
|
|
||||||
authSvc, err := auth.NewAuthService(database, queries, cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create auth service: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rebacSvc := service.NewRebacService(queries)
|
|
||||||
placementSvc := service.NewPlacementService(queries)
|
|
||||||
syncSvc := service.NewSyncService(queries)
|
|
||||||
|
|
||||||
ocrSvc.Start(cfg.OCRWorkers)
|
|
||||||
defer ocrSvc.Stop()
|
|
||||||
|
|
||||||
conversionSvc.Start(cfg.ConversionWorkers)
|
|
||||||
defer conversionSvc.Stop()
|
|
||||||
|
|
||||||
h := handler.New(database, resourceSvc, ocrSvc, urlSvc, auth.NewAuthHandler(authSvc), conversionSvc, rebacSvc, placementSvc, syncSvc, eventBroker)
|
|
||||||
|
|
||||||
r := gin.Default()
|
|
||||||
handler.SetupRoutes(r, h, authSvc)
|
|
||||||
|
|
||||||
log.Printf("Server starting on port %s", cfg.Port)
|
|
||||||
if err := r.Run(":" + cfg.Port); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
DocumentName string
|
||||||
|
DocumentType int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ERROR_DOCUMENT_TYPE = "ERROR_DOCUMENT_TYPE"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FILE = 1
|
||||||
|
DIRECTORY = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsDocumentTypeValid(documentType int) bool {
|
||||||
|
|
||||||
|
return IsFile(documentType) || IsDirectory(documentType)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFile(documentType int) bool {
|
||||||
|
return FILE == documentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDirectory(documentType int) bool {
|
||||||
|
return DIRECTORY == documentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocument(documentName string, documentType int) (error, *Document) {
|
||||||
|
|
||||||
|
documentTypeIsValid := IsDocumentTypeValid(documentType)
|
||||||
|
|
||||||
|
if !documentTypeIsValid {
|
||||||
|
return fmt.Errorf(ERROR_DOCUMENT_TYPE), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, &Document{
|
||||||
|
DocumentName: documentName,
|
||||||
|
DocumentType: documentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthHandler struct {
|
|
||||||
auth *AuthService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthHandler(auth *AuthService) *AuthHandler {
|
|
||||||
return &AuthHandler{auth: auth}
|
|
||||||
}
|
|
||||||
|
|
||||||
type registerRequest struct {
|
|
||||||
Username string `json:"username" binding:"required"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type loginRequest struct {
|
|
||||||
Username string `json:"username" binding:"required"`
|
|
||||||
Password string `json:"password" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type refreshRequest struct {
|
|
||||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type logoutRequest struct {
|
|
||||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authResponse struct {
|
|
||||||
User UserResponse `json:"user"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type tokenResponse struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) Register(c *gin.Context) {
|
|
||||||
var req registerRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_INPUT", "username and password are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, user, err := h.auth.Register(c.Request.Context(), req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, ErrUsernameTaken):
|
|
||||||
api.Error(c, http.StatusConflict, "USERNAME_TAKEN", "username already taken")
|
|
||||||
default:
|
|
||||||
api.Error(c, http.StatusBadRequest, "VALIDATION_ERROR", err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, authResponse{
|
|
||||||
User: *user,
|
|
||||||
AccessToken: tokens.AccessToken,
|
|
||||||
RefreshToken: tokens.RefreshToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) Login(c *gin.Context) {
|
|
||||||
var req loginRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_INPUT", "username and password are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, user, err := h.auth.Login(c.Request.Context(), req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, ErrInvalidCredentials):
|
|
||||||
api.Error(c, http.StatusUnauthorized, "INVALID_CREDENTIALS", "invalid username or password")
|
|
||||||
default:
|
|
||||||
api.Error(c, http.StatusInternalServerError, "INTERNAL_ERROR", "something went wrong")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, authResponse{
|
|
||||||
User: *user,
|
|
||||||
AccessToken: tokens.AccessToken,
|
|
||||||
RefreshToken: tokens.RefreshToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) Refresh(c *gin.Context) {
|
|
||||||
var req refreshRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_INPUT", "refresh_token is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := h.auth.Refresh(c.Request.Context(), req.RefreshToken)
|
|
||||||
if err != nil {
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, ErrInvalidToken):
|
|
||||||
api.Error(c, http.StatusUnauthorized, "INVALID_TOKEN", "invalid or expired refresh token")
|
|
||||||
default:
|
|
||||||
api.Error(c, http.StatusInternalServerError, "INTERNAL_ERROR", "something went wrong")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, tokenResponse{
|
|
||||||
AccessToken: tokens.AccessToken,
|
|
||||||
RefreshToken: tokens.RefreshToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
||||||
var req logoutRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_INPUT", "refresh_token is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.auth.Logout(c.Request.Context(), req.RefreshToken); err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "INTERNAL_ERROR", "something went wrong")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
const UserIDKey = "userID"
|
|
||||||
|
|
||||||
func (s *AuthService) RequireAuth() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
header := c.GetHeader("Authorization")
|
|
||||||
if header == "" {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
|
||||||
"error": gin.H{"code": "MISSING_TOKEN", "message": "authorization header required"},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(header, " ", 2)
|
|
||||||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
|
||||||
"error": gin.H{"code": "INVALID_TOKEN", "message": "invalid authorization format"},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, err := s.ValidateAccessToken(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
|
||||||
"error": gin.H{"code": "INVALID_TOKEN", "message": "invalid or expired token"},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set(UserIDKey, claims)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"aidanwoods.dev/go-paseto"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
"golang.org/x/crypto/argon2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
|
||||||
ErrUsernameTaken = errors.New("username already taken")
|
|
||||||
ErrInvalidToken = errors.New("invalid or expired token")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
accessTokenTTL = 30 * time.Minute
|
|
||||||
refreshTokenTTL = 7 * 24 * time.Hour
|
|
||||||
saltLength = 16
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthService struct {
|
|
||||||
db *sql.DB
|
|
||||||
queries *db.Queries
|
|
||||||
key paseto.V4SymmetricKey
|
|
||||||
parser *paseto.Parser
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenPair struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthService(database *sql.DB, queries *db.Queries, cfg *config.Config) (*AuthService, error) {
|
|
||||||
key, err := paseto.V4SymmetricKeyFromHex(cfg.PASETOKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid paseto key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parser := paseto.NewParser()
|
|
||||||
parser.AddRule(paseto.NotExpired())
|
|
||||||
|
|
||||||
return &AuthService{
|
|
||||||
db: database,
|
|
||||||
queries: queries,
|
|
||||||
key: key,
|
|
||||||
parser: &parser,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) Register(ctx context.Context, username, password string) (*TokenPair, *UserResponse, error) {
|
|
||||||
username = strings.TrimSpace(username)
|
|
||||||
if len(username) < 3 || len(username) > 30 {
|
|
||||||
return nil, nil, fmt.Errorf("username must be 3-30 characters")
|
|
||||||
}
|
|
||||||
if len(password) < 8 {
|
|
||||||
return nil, nil, fmt.Errorf("password must be at least 8 characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := s.queries.GetUserByUsername(ctx, username)
|
|
||||||
if err == nil && existing.ID != uuid.Nil {
|
|
||||||
return nil, nil, ErrUsernameTaken
|
|
||||||
}
|
|
||||||
|
|
||||||
hash, err := hashPassword(password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("hash password: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("begin tx: %w", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
qtx := s.queries.WithTx(tx)
|
|
||||||
|
|
||||||
user, err := qtx.CreateUser(ctx, db.CreateUserParams{
|
|
||||||
Username: username,
|
|
||||||
PasswordHash: hash,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("create user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := qtx.CreateStorageLocation(ctx, db.CreateStorageLocationParams{
|
|
||||||
UserID: user.ID,
|
|
||||||
DeviceName: "VaultDrop Server",
|
|
||||||
Role: "server",
|
|
||||||
}); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("create server location: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, err := s.createAccessToken(user.ID.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("create access token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshToken, err := s.createRefreshToken(user.ID.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("create refresh token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshHash := hashToken(refreshToken)
|
|
||||||
if _, err := qtx.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{
|
|
||||||
UserID: user.ID,
|
|
||||||
TokenHash: refreshHash,
|
|
||||||
ExpiresAt: time.Now().Add(refreshTokenTTL),
|
|
||||||
}); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("store refresh token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("commit tx: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &TokenPair{
|
|
||||||
AccessToken: accessToken,
|
|
||||||
RefreshToken: refreshToken,
|
|
||||||
}, &UserResponse{ID: user.ID.String(), Username: user.Username}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) Login(ctx context.Context, username, password string) (*TokenPair, *UserResponse, error) {
|
|
||||||
user, err := s.queries.GetUserByUsername(ctx, strings.TrimSpace(username))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, ErrInvalidCredentials
|
|
||||||
}
|
|
||||||
|
|
||||||
if !verifyPassword(password, user.PasswordHash) {
|
|
||||||
return nil, nil, ErrInvalidCredentials
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := s.generateTokens(ctx, user.ID.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("generate tokens: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens, &UserResponse{ID: user.ID.String(), Username: user.Username}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) Refresh(ctx context.Context, refreshToken string) (*TokenPair, error) {
|
|
||||||
userID, err := s.validateRefreshToken(refreshToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenHash := hashToken(refreshToken)
|
|
||||||
stored, err := s.queries.GetRefreshToken(ctx, tokenHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if stored.UserID.String() != userID {
|
|
||||||
return nil, ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.queries.RevokeRefreshToken(ctx, tokenHash); err != nil {
|
|
||||||
return nil, fmt.Errorf("revoke refresh token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokens, err := s.generateTokens(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("generate tokens: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) Logout(ctx context.Context, refreshToken string) error {
|
|
||||||
tokenHash := hashToken(refreshToken)
|
|
||||||
return s.queries.RevokeRefreshToken(ctx, tokenHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) ValidateAccessToken(token string) (string, error) {
|
|
||||||
parsed, err := s.parser.ParseV4Local(s.key, token, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := parsed.GetString("user_id")
|
|
||||||
if err != nil || userID == "" {
|
|
||||||
return "", ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return userID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) generateTokens(ctx context.Context, userID string) (*TokenPair, error) {
|
|
||||||
accessToken, err := s.createAccessToken(userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshToken, err := s.createRefreshToken(userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshHash := hashToken(refreshToken)
|
|
||||||
userUUID := uuid.MustParse(userID)
|
|
||||||
_, err = s.queries.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{
|
|
||||||
UserID: userUUID,
|
|
||||||
TokenHash: refreshHash,
|
|
||||||
ExpiresAt: time.Now().Add(refreshTokenTTL),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("store refresh token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &TokenPair{
|
|
||||||
AccessToken: accessToken,
|
|
||||||
RefreshToken: refreshToken,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) createAccessToken(userID string) (string, error) {
|
|
||||||
token := paseto.NewToken()
|
|
||||||
token.Set("user_id", userID)
|
|
||||||
token.SetExpiration(time.Now().Add(accessTokenTTL))
|
|
||||||
return token.V4Encrypt(s.key, nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) createRefreshToken(userID string) (string, error) {
|
|
||||||
token := paseto.NewToken()
|
|
||||||
token.Set("user_id", userID)
|
|
||||||
token.SetExpiration(time.Now().Add(refreshTokenTTL))
|
|
||||||
return token.V4Encrypt(s.key, nil), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AuthService) validateRefreshToken(token string) (string, error) {
|
|
||||||
parsed, err := s.parser.ParseV4Local(s.key, token, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := parsed.GetString("user_id")
|
|
||||||
if err != nil || userID == "" {
|
|
||||||
return "", ErrInvalidToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return userID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashPassword(password string) (string, error) {
|
|
||||||
salt := make([]byte, saltLength)
|
|
||||||
if _, err := rand.Read(salt); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
|
|
||||||
return fmt.Sprintf("$argon2id$v=19$m=65536,t=1,p=4$%s$%s",
|
|
||||||
base64.RawStdEncoding.EncodeToString(salt),
|
|
||||||
base64.RawStdEncoding.EncodeToString(hash),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyPassword(password, encodedHash string) bool {
|
|
||||||
parts := strings.Split(encodedHash, "$")
|
|
||||||
if len(parts) != 6 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
|
|
||||||
return sha256.Sum256(hash) == sha256.Sum256(expectedHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashToken(token string) string {
|
|
||||||
h := sha256.Sum256([]byte(token))
|
|
||||||
return base64.RawURLEncoding.EncodeToString(h[:])
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Port string
|
|
||||||
DatabaseURL string
|
|
||||||
OCREndpoint string
|
|
||||||
UploadDir string
|
|
||||||
HMACSecret string
|
|
||||||
ServerHost string
|
|
||||||
PASETOKey string
|
|
||||||
LibreOfficePath string
|
|
||||||
PdftoppmPath string
|
|
||||||
ThumbnailDir string
|
|
||||||
URLExpiryMinutes int
|
|
||||||
OCRWorkers int
|
|
||||||
ConversionWorkers int
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load() *Config {
|
|
||||||
return &Config{
|
|
||||||
Port: envOr("PORT", "8080"),
|
|
||||||
DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"),
|
|
||||||
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
|
|
||||||
UploadDir: envOr("UPLOAD_DIR", "./uploads"),
|
|
||||||
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
|
|
||||||
ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"),
|
|
||||||
PASETOKey: envOr("PASETO_KEY", "01234567890123456789012345678901234567890123456789012345678901234"),
|
|
||||||
LibreOfficePath: envOr("LIBREOFFICE_PATH", "/usr/bin/libreoffice"),
|
|
||||||
PdftoppmPath: envOr("PDFTOPPM_PATH", "/usr/bin/pdftoppm"),
|
|
||||||
ThumbnailDir: envOr("THUMBNAIL_DIR", "./uploads/thumbnails"),
|
|
||||||
URLExpiryMinutes: envOrInt("URL_EXPIRY_MINUTES", 60),
|
|
||||||
OCRWorkers: envOrInt("OCR_WORKERS", 1),
|
|
||||||
ConversionWorkers: envOrInt("CONVERSION_WORKERS", 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func envOrInt(key string, fallback int) int {
|
|
||||||
if v := os.Getenv(key); v != "" {
|
|
||||||
var n int
|
|
||||||
if _, err := fmt.Sscanf(v, "%d", &n); err == nil && n > 0 {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func envOr(key, fallback string) string {
|
|
||||||
if v := os.Getenv(key); v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: auth.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createRefreshToken = `-- name: CreateRefreshToken :one
|
|
||||||
INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
RETURNING id, user_id, token_hash, expires_at, revoked, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateRefreshTokenParams struct {
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
TokenHash string `json:"token_hash"`
|
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createRefreshToken, arg.UserID, arg.TokenHash, arg.ExpiresAt)
|
|
||||||
var i RefreshToken
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.TokenHash,
|
|
||||||
&i.ExpiresAt,
|
|
||||||
&i.Revoked,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const createUser = `-- name: CreateUser :one
|
|
||||||
INSERT INTO users (username, password_hash)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
RETURNING id, username, password_hash, parent_user_id, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateUserParams struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
PasswordHash string `json:"password_hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.PasswordHash)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Username,
|
|
||||||
&i.PasswordHash,
|
|
||||||
&i.ParentUserID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRefreshToken = `-- name: GetRefreshToken :one
|
|
||||||
SELECT id, user_id, token_hash, expires_at, revoked, created_at FROM refresh_tokens
|
|
||||||
WHERE token_hash = $1 AND revoked = FALSE AND expires_at > NOW()
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetRefreshToken(ctx context.Context, tokenHash string) (RefreshToken, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getRefreshToken, tokenHash)
|
|
||||||
var i RefreshToken
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.TokenHash,
|
|
||||||
&i.ExpiresAt,
|
|
||||||
&i.Revoked,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
|
||||||
SELECT id, username, password_hash, parent_user_id, created_at, updated_at FROM users WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Username,
|
|
||||||
&i.PasswordHash,
|
|
||||||
&i.ParentUserID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
|
||||||
SELECT id, username, password_hash, parent_user_id, created_at, updated_at FROM users WHERE username = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getUserByUsername, username)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Username,
|
|
||||||
&i.PasswordHash,
|
|
||||||
&i.ParentUserID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const revokeAllUserRefreshTokens = `-- name: RevokeAllUserRefreshTokens :exec
|
|
||||||
UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) RevokeAllUserRefreshTokens(ctx context.Context, userID uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, revokeAllUserRefreshTokens, userID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const revokeRefreshToken = `-- name: RevokeRefreshToken :exec
|
|
||||||
UPDATE refresh_tokens SET revoked = TRUE WHERE token_hash = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) RevokeRefreshToken(ctx context.Context, tokenHash string) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, revokeRefreshToken, tokenHash)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
const driver = "postgres"
|
|
||||||
|
|
||||||
func Connect() (*sql.DB, error) {
|
|
||||||
dsn := os.Getenv("DATABASE_URL")
|
|
||||||
if dsn == "" {
|
|
||||||
dsn = "postgres://localhost:5432/vaultdrop?sslmode=disable"
|
|
||||||
}
|
|
||||||
|
|
||||||
database, err := sql.Open(driver, dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("open db: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
database.SetMaxOpenConns(25)
|
|
||||||
|
|
||||||
if err := database.Ping(); err != nil {
|
|
||||||
return nil, fmt.Errorf("ping db: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return database, nil
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBTX interface {
|
|
||||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
|
||||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
|
||||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
|
||||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(db DBTX) *Queries {
|
|
||||||
return &Queries{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Queries struct {
|
|
||||||
db DBTX
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
|
||||||
return &Queries{
|
|
||||||
db: tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: health.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const getHealth = `-- name: GetHealth :one
|
|
||||||
SELECT 1 AS ok
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetHealth(ctx context.Context) (int32, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getHealth)
|
|
||||||
var ok int32
|
|
||||||
err := row.Scan(&ok)
|
|
||||||
return ok, err
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
|
||||||
"github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunMigrations(migrationsURL string) error {
|
|
||||||
dsn := os.Getenv("DATABASE_URL")
|
|
||||||
if dsn == "" {
|
|
||||||
dsn = "postgres://localhost:5432/vaultdrop?sslmode=disable"
|
|
||||||
}
|
|
||||||
|
|
||||||
migDB, err := sql.Open("postgres", dsn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open migration db: %w", err)
|
|
||||||
}
|
|
||||||
defer migDB.Close()
|
|
||||||
|
|
||||||
sourceDriver, err := (&file.File{}).Open(migrationsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open migrations source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbDriver, err := postgres.WithInstance(migDB, &postgres.Config{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create postgres driver: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := migrate.NewWithInstance("file", sourceDriver, "postgres", dbDriver)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create migrate instance: %w", err)
|
|
||||||
}
|
|
||||||
defer m.Close()
|
|
||||||
|
|
||||||
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
|
||||||
return fmt.Errorf("run migrations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Database migrations applied successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
-- Rollback 001_init
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- VaultDrop 001: Initial schema
|
|
||||||
-- This is a placeholder. Add your first CREATE TABLE here.
|
|
||||||
SELECT 1;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS files;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- VaultDrop 002: Files table
|
|
||||||
CREATE TABLE files (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL DEFAULT '',
|
|
||||||
size BIGINT NOT NULL DEFAULT 0,
|
|
||||||
storage_key TEXT NOT NULL,
|
|
||||||
checksum TEXT NOT NULL DEFAULT '',
|
|
||||||
ocr_text TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS file_tags;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS tags;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
-- VaultDrop 003: tags table
|
|
||||||
|
|
||||||
CREATE TABLE tags (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
parent_tag_id TEXT,
|
|
||||||
tag_name TEXT NOT NULL, -- exemple: Brice, vélo, facture ...
|
|
||||||
tag_type TEXT NOT NULL DEFAULT 'none', -- exemple: entity, none
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT fk_parent_tag FOREIGN KEY(parent_tag_id) REFERENCES tags(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE file_tags (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
tag_id TEXT,
|
|
||||||
file_id TEXT,
|
|
||||||
CONSTRAINT fk_tag FOREIGN KEY(tag_id) REFERENCES tags(id),
|
|
||||||
CONSTRAINT fk_file FOREIGN KEY(file_id) REFERENCES files(id)
|
|
||||||
)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- VaultDrop 003: tags table
|
|
||||||
ALTER TABLE files DROP COLUMN IF EXISTS is_folder;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE files DROP CONSTRAINT IF EXISTS fk_parent_file_id
|
|
||||||
|
|
||||||
ALTER TABLE files DROP COLUMN IF EXISTS parent_file_id;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE files ADD COLUMN parent_file_id TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE files ADD CONSTRAINT fk_parent_file_id FOREIGN KEY(parent_file_id) REFERENCES files(id)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- VaultDrop 003: tags table
|
|
||||||
ALTER TABLE files DROP COLUMN IF EXISTS is_folder;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE files DROP COLUMN IF EXISTS is_folder;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- VaultDrop 007: re-add is_folder to distinguish folders from files
|
|
||||||
ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE files ALTER COLUMN id DROP DEFAULT;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE files ALTER COLUMN id SET DEFAULT gen_random_uuid();
|
|
||||||
ALTER TABLE files ALTER COLUMN storage_key SET DEFAULT '';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
CREATE TABLE users (
|
|
||||||
id TEXT NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS users;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS refresh_tokens;
|
|
||||||
DROP TABLE IF EXISTS users;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
CREATE TABLE users (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE refresh_tokens (
|
|
||||||
id TEXT NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT NOT NULL,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
|
||||||
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS thumbnails;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE thumbnails (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
||||||
page_number INTEGER NOT NULL,
|
|
||||||
resolution_label TEXT NOT NULL,
|
|
||||||
width INTEGER NOT NULL,
|
|
||||||
height INTEGER NOT NULL,
|
|
||||||
storage_key TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_thumbnails_file_id ON thumbnails(file_id);
|
|
||||||
CREATE UNIQUE INDEX idx_thumbnails_unique ON thumbnails(file_id, page_number, resolution_label);
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
-- VaultDrop 012 down: revert to V1 schema
|
|
||||||
DROP FUNCTION IF EXISTS resolve_effective_role;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS sync_queue CASCADE;
|
|
||||||
DROP TABLE IF EXISTS retention_policies CASCADE;
|
|
||||||
DROP TABLE IF EXISTS rebac_relations CASCADE;
|
|
||||||
DROP TABLE IF EXISTS resource_placements CASCADE;
|
|
||||||
DROP TABLE IF EXISTS resource_variants CASCADE;
|
|
||||||
DROP TABLE IF EXISTS resource_tags CASCADE;
|
|
||||||
DROP TABLE IF EXISTS refresh_tokens CASCADE;
|
|
||||||
DROP TABLE IF EXISTS storage_locations CASCADE;
|
|
||||||
DROP TABLE IF EXISTS resources CASCADE;
|
|
||||||
DROP TABLE IF EXISTS tags CASCADE;
|
|
||||||
DROP TABLE IF EXISTS users CASCADE;
|
|
||||||
|
|
||||||
-- Restore V1 tables
|
|
||||||
CREATE TABLE users (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE tags (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
parent_tag_id TEXT,
|
|
||||||
tag_name TEXT NOT NULL,
|
|
||||||
tag_type TEXT NOT NULL DEFAULT 'none',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE files (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL DEFAULT '',
|
|
||||||
size BIGINT NOT NULL DEFAULT 0,
|
|
||||||
storage_key TEXT NOT NULL DEFAULT '',
|
|
||||||
checksum TEXT NOT NULL DEFAULT '',
|
|
||||||
ocr_text TEXT NOT NULL DEFAULT '',
|
|
||||||
is_folder BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
parent_file_id TEXT,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE file_tags (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
tag_id TEXT,
|
|
||||||
file_id TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE thumbnails (
|
|
||||||
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
||||||
page_number INTEGER NOT NULL,
|
|
||||||
resolution_label TEXT NOT NULL,
|
|
||||||
width INTEGER NOT NULL,
|
|
||||||
height INTEGER NOT NULL,
|
|
||||||
storage_key TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE refresh_tokens (
|
|
||||||
id TEXT NOT NULL DEFAULT gen_random_uuid(),
|
|
||||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT NOT NULL,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
-- VaultDrop 012: Clean V3 schema
|
|
||||||
-- Drops all legacy V1 tables, recreates everything with UUIDs
|
|
||||||
|
|
||||||
-- Drop legacy tables (order matters for FK dependencies)
|
|
||||||
DROP TABLE IF EXISTS file_tags CASCADE;
|
|
||||||
DROP TABLE IF EXISTS resource_tags CASCADE;
|
|
||||||
DROP TABLE IF EXISTS thumbnails CASCADE;
|
|
||||||
DROP TABLE IF EXISTS refresh_tokens CASCADE;
|
|
||||||
DROP TABLE IF EXISTS files CASCADE;
|
|
||||||
DROP TABLE IF EXISTS tags CASCADE;
|
|
||||||
DROP TABLE IF EXISTS users CASCADE;
|
|
||||||
|
|
||||||
-- Level 1: No dependencies
|
|
||||||
CREATE TABLE users (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
parent_user_id UUID REFERENCES users(id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE tags (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
parent_tag_id UUID REFERENCES tags(id),
|
|
||||||
tag_name TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Level 2: Depend on users
|
|
||||||
CREATE TABLE resources (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL DEFAULT '',
|
|
||||||
size BIGINT NOT NULL DEFAULT 0,
|
|
||||||
checksum TEXT NOT NULL DEFAULT '',
|
|
||||||
ocr_text TEXT NOT NULL DEFAULT '',
|
|
||||||
is_folder BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
parent_resource_id UUID REFERENCES resources(id),
|
|
||||||
owner_id UUID NOT NULL REFERENCES users(id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_resources_owner ON resources(owner_id);
|
|
||||||
CREATE INDEX idx_resources_parent ON resources(parent_resource_id);
|
|
||||||
CREATE INDEX idx_resources_checksum_owner ON resources(checksum, owner_id);
|
|
||||||
|
|
||||||
CREATE TABLE storage_locations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
|
||||||
device_name TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL CHECK (role IN ('primary', 'device', 'backup', 'server')),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
last_seen_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_locations_user ON storage_locations(user_id);
|
|
||||||
|
|
||||||
CREATE TABLE refresh_tokens (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT NOT NULL,
|
|
||||||
expires_at TIMESTAMP NOT NULL,
|
|
||||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
|
||||||
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
|
|
||||||
|
|
||||||
-- Level 3: Depend on level 1-2
|
|
||||||
CREATE TABLE resource_tags (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
tag_id UUID NOT NULL REFERENCES tags(id),
|
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE(tag_id, resource_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_resource_tags_tag ON resource_tags(tag_id);
|
|
||||||
CREATE INDEX idx_resource_tags_resource ON resource_tags(resource_id);
|
|
||||||
|
|
||||||
CREATE TABLE resource_variants (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
variant_type TEXT NOT NULL,
|
|
||||||
page_number INTEGER NOT NULL DEFAULT 1,
|
|
||||||
width INTEGER NOT NULL DEFAULT 0,
|
|
||||||
height INTEGER NOT NULL DEFAULT 0,
|
|
||||||
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
|
|
||||||
generated_by TEXT NOT NULL DEFAULT 'server' CHECK (generated_by IN ('server', 'client')),
|
|
||||||
storage_key TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_variants_resource ON resource_variants(resource_id);
|
|
||||||
CREATE UNIQUE INDEX idx_variants_resource_type_page ON resource_variants(resource_id, variant_type, page_number);
|
|
||||||
|
|
||||||
CREATE TABLE resource_placements (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
storage_location_id UUID NOT NULL REFERENCES storage_locations(id),
|
|
||||||
status TEXT NOT NULL DEFAULT 'synced' CHECK (status IN ('local_only', 'synced', 'cloud_only', 'pending_upload', 'pending_download')),
|
|
||||||
storage_key TEXT,
|
|
||||||
synced_at TIMESTAMP,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(resource_id, storage_location_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_placements_resource ON resource_placements(resource_id);
|
|
||||||
CREATE INDEX idx_placements_location ON resource_placements(storage_location_id);
|
|
||||||
CREATE INDEX idx_placements_status ON resource_placements(status);
|
|
||||||
|
|
||||||
CREATE TABLE rebac_relations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
subject_user_id UUID NOT NULL REFERENCES users(id),
|
|
||||||
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'editor', 'viewer')),
|
|
||||||
granted_by UUID NOT NULL REFERENCES users(id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(resource_id, subject_user_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_rebac_resource ON rebac_relations(resource_id);
|
|
||||||
CREATE INDEX idx_rebac_subject ON rebac_relations(subject_user_id);
|
|
||||||
|
|
||||||
CREATE TABLE retention_policies (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
|
||||||
storage_location_id UUID NOT NULL REFERENCES storage_locations(id),
|
|
||||||
rule_type TEXT NOT NULL,
|
|
||||||
rule_value JSONB NOT NULL DEFAULT '{}',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_policies_user ON retention_policies(user_id);
|
|
||||||
CREATE INDEX idx_policies_location ON retention_policies(storage_location_id);
|
|
||||||
|
|
||||||
CREATE TABLE sync_queue (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
storage_location_id UUID NOT NULL REFERENCES storage_locations(id),
|
|
||||||
operation TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending',
|
|
||||||
attempts INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_queue_status ON sync_queue(status);
|
|
||||||
CREATE INDEX idx_queue_resource ON sync_queue(resource_id);
|
|
||||||
CREATE INDEX idx_queue_location ON sync_queue(storage_location_id);
|
|
||||||
|
|
||||||
-- ReBAC permission resolver
|
|
||||||
CREATE OR REPLACE FUNCTION resolve_effective_role(p_user_id UUID, p_resource_id UUID)
|
|
||||||
RETURNS TEXT AS $$
|
|
||||||
DECLARE
|
|
||||||
v_role TEXT;
|
|
||||||
BEGIN
|
|
||||||
WITH RECURSIVE rtree AS (
|
|
||||||
SELECT r.id, r.parent_resource_id, r.owner_id
|
|
||||||
FROM resources r
|
|
||||||
WHERE r.id = p_resource_id
|
|
||||||
UNION ALL
|
|
||||||
SELECT r.id, r.parent_resource_id, r.owner_id
|
|
||||||
FROM resources r
|
|
||||||
JOIN rtree ON r.id = rtree.parent_resource_id
|
|
||||||
)
|
|
||||||
SELECT CASE
|
|
||||||
WHEN EXISTS(SELECT 1 FROM rtree WHERE owner_id = p_user_id) THEN 'owner'
|
|
||||||
ELSE COALESCE(
|
|
||||||
(SELECT rr.role::text FROM rebac_relations rr
|
|
||||||
JOIN rtree ON rr.resource_id = rtree.id
|
|
||||||
WHERE rr.subject_user_id = p_user_id
|
|
||||||
ORDER BY CASE rr.role
|
|
||||||
WHEN 'owner' THEN 0
|
|
||||||
WHEN 'admin' THEN 1
|
|
||||||
WHEN 'editor' THEN 2
|
|
||||||
WHEN 'viewer' THEN 3
|
|
||||||
END ASC LIMIT 1),
|
|
||||||
''
|
|
||||||
)
|
|
||||||
END INTO v_role;
|
|
||||||
RETURN v_role;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_storage_locations_user_server;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_storage_locations_user_server
|
|
||||||
ON storage_locations(user_id) WHERE role = 'server';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS ocr_jobs;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
CREATE TABLE ocr_jobs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
||||||
file_path TEXT NOT NULL DEFAULT '',
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'done', 'failed')),
|
|
||||||
error_message TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_ocr_jobs_status ON ocr_jobs(status);
|
|
||||||
CREATE INDEX idx_ocr_jobs_resource ON ocr_jobs(resource_id);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE resource_tags DROP CONSTRAINT resource_tags_resource_id_fkey,
|
|
||||||
ADD CONSTRAINT resource_tags_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES resources(id);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE resource_tags DROP CONSTRAINT resource_tags_resource_id_fkey,
|
|
||||||
ADD CONSTRAINT resource_tags_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE;
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OcrJob struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
FilePath string `json:"file_path"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ErrorMessage string `json:"error_message"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RebacRelation struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
SubjectUserID uuid.UUID `json:"subject_user_id"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
GrantedBy uuid.UUID `json:"granted_by"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RefreshToken struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
TokenHash string `json:"token_hash"`
|
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
|
||||||
Revoked bool `json:"revoked"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resource struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
OcrText string `json:"ocr_text"`
|
|
||||||
IsFolder bool `json:"is_folder"`
|
|
||||||
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourcePlacement struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
StorageKey sql.NullString `json:"storage_key"`
|
|
||||||
SyncedAt sql.NullTime `json:"synced_at"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceTag struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
TagID uuid.UUID `json:"tag_id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceVariant struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
VariantType string `json:"variant_type"`
|
|
||||||
PageNumber int32 `json:"page_number"`
|
|
||||||
Width int32 `json:"width"`
|
|
||||||
Height int32 `json:"height"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
GeneratedBy string `json:"generated_by"`
|
|
||||||
StorageKey string `json:"storage_key"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RetentionPolicy struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
RuleType string `json:"rule_type"`
|
|
||||||
RuleValue json.RawMessage `json:"rule_value"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorageLocation struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
DeviceName string `json:"device_name"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
LastSeenAt sql.NullTime `json:"last_seen_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyncQueue struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
Operation string `json:"operation"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Attempts int32 `json:"attempts"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tag struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
ParentTagID uuid.NullUUID `json:"parent_tag_id"`
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
PasswordHash string `json:"password_hash"`
|
|
||||||
ParentUserID uuid.NullUUID `json:"parent_user_id"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: ocr_jobs.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createOCRJob = `-- name: CreateOCRJob :one
|
|
||||||
INSERT INTO ocr_jobs (resource_id, file_path, status)
|
|
||||||
VALUES ($1, $2, 'pending')
|
|
||||||
RETURNING id, resource_id, file_path, status, error_message, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateOCRJobParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
FilePath string `json:"file_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateOCRJob(ctx context.Context, arg CreateOCRJobParams) (OcrJob, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createOCRJob, arg.ResourceID, arg.FilePath)
|
|
||||||
var i OcrJob
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.FilePath,
|
|
||||||
&i.Status,
|
|
||||||
&i.ErrorMessage,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteOCRJob = `-- name: DeleteOCRJob :exec
|
|
||||||
DELETE FROM ocr_jobs
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteOCRJob(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteOCRJob, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOCRJob = `-- name: GetOCRJob :one
|
|
||||||
SELECT id, resource_id, file_path, status, error_message, created_at, updated_at FROM ocr_jobs
|
|
||||||
WHERE id = $1 LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetOCRJob(ctx context.Context, id uuid.UUID) (OcrJob, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getOCRJob, id)
|
|
||||||
var i OcrJob
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.FilePath,
|
|
||||||
&i.Status,
|
|
||||||
&i.ErrorMessage,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listPendingOCRJobs = `-- name: ListPendingOCRJobs :many
|
|
||||||
SELECT id, resource_id, file_path, status, error_message, created_at, updated_at FROM ocr_jobs
|
|
||||||
WHERE status = 'pending'
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListPendingOCRJobs(ctx context.Context) ([]OcrJob, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listPendingOCRJobs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []OcrJob
|
|
||||||
for rows.Next() {
|
|
||||||
var i OcrJob
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.FilePath,
|
|
||||||
&i.Status,
|
|
||||||
&i.ErrorMessage,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateOCRJobStatus = `-- name: UpdateOCRJobStatus :exec
|
|
||||||
UPDATE ocr_jobs
|
|
||||||
SET status = $1, error_message = $2, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateOCRJobStatusParams struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
ErrorMessage string `json:"error_message"`
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateOCRJobStatus(ctx context.Context, arg UpdateOCRJobStatusParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updateOCRJobStatus, arg.Status, arg.ErrorMessage, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
-- name: CreateUser :one
|
|
||||||
INSERT INTO users (username, password_hash)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetUserByUsername :one
|
|
||||||
SELECT * FROM users WHERE username = $1;
|
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
|
||||||
SELECT * FROM users WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: CreateRefreshToken :one
|
|
||||||
INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetRefreshToken :one
|
|
||||||
SELECT * FROM refresh_tokens
|
|
||||||
WHERE token_hash = $1 AND revoked = FALSE AND expires_at > NOW();
|
|
||||||
|
|
||||||
-- name: RevokeRefreshToken :exec
|
|
||||||
UPDATE refresh_tokens SET revoked = TRUE WHERE token_hash = $1;
|
|
||||||
|
|
||||||
-- name: RevokeAllUserRefreshTokens :exec
|
|
||||||
UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = $1;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- name: GetHealth :one
|
|
||||||
SELECT 1 AS ok;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
-- name: CreateOCRJob :one
|
|
||||||
INSERT INTO ocr_jobs (resource_id, file_path, status)
|
|
||||||
VALUES ($1, $2, 'pending')
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetOCRJob :one
|
|
||||||
SELECT * FROM ocr_jobs
|
|
||||||
WHERE id = $1 LIMIT 1;
|
|
||||||
|
|
||||||
-- name: ListPendingOCRJobs :many
|
|
||||||
SELECT * FROM ocr_jobs
|
|
||||||
WHERE status = 'pending'
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
|
|
||||||
-- name: UpdateOCRJobStatus :exec
|
|
||||||
UPDATE ocr_jobs
|
|
||||||
SET status = $1, error_message = $2, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $3;
|
|
||||||
|
|
||||||
-- name: DeleteOCRJob :exec
|
|
||||||
DELETE FROM ocr_jobs
|
|
||||||
WHERE id = $1;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
-- name: CreateRebacRelation :one
|
|
||||||
INSERT INTO rebac_relations (resource_id, subject_user_id, role, granted_by)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetRebacRelation :one
|
|
||||||
SELECT * FROM rebac_relations
|
|
||||||
WHERE resource_id = $1 AND subject_user_id = $2
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: ListRebacRelationsByResource :many
|
|
||||||
SELECT * FROM rebac_relations
|
|
||||||
WHERE resource_id = $1
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
|
|
||||||
-- name: ListRebacRelationsBySubject :many
|
|
||||||
SELECT * FROM rebac_relations
|
|
||||||
WHERE subject_user_id = $1
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
|
|
||||||
-- name: DeleteRebacRelation :exec
|
|
||||||
DELETE FROM rebac_relations
|
|
||||||
WHERE resource_id = $1 AND subject_user_id = $2;
|
|
||||||
|
|
||||||
-- name: DeleteRebacRelationsByResource :exec
|
|
||||||
DELETE FROM rebac_relations
|
|
||||||
WHERE resource_id = $1;
|
|
||||||
|
|
||||||
-- name: HasRebacRelation :one
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM rebac_relations
|
|
||||||
WHERE resource_id = $1 AND subject_user_id = $2 AND role = $3
|
|
||||||
);
|
|
||||||
|
|
||||||
-- name: ResolveEffectiveRole :one
|
|
||||||
SELECT resolve_effective_role($1, $2) AS role;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
-- name: CreatePlacement :one
|
|
||||||
INSERT INTO resource_placements (resource_id, storage_location_id, status, storage_key, synced_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetPlacement :one
|
|
||||||
SELECT * FROM resource_placements
|
|
||||||
WHERE resource_id = $1 AND storage_location_id = $2
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: ListPlacementsByResource :many
|
|
||||||
SELECT * FROM resource_placements
|
|
||||||
WHERE resource_id = $1;
|
|
||||||
|
|
||||||
-- name: ListPlacementsByLocation :many
|
|
||||||
SELECT * FROM resource_placements
|
|
||||||
WHERE storage_location_id = $1;
|
|
||||||
|
|
||||||
-- name: UpdatePlacementStatus :exec
|
|
||||||
UPDATE resource_placements
|
|
||||||
SET status = $1, synced_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2;
|
|
||||||
|
|
||||||
-- name: DeletePlacement :exec
|
|
||||||
DELETE FROM resource_placements
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetServerPlacementByResource :one
|
|
||||||
SELECT rp.* FROM resource_placements rp
|
|
||||||
JOIN storage_locations sl ON sl.id = rp.storage_location_id
|
|
||||||
WHERE rp.resource_id = $1 AND sl.role = 'server'
|
|
||||||
LIMIT 1;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
-- name: CreateResourceVariant :one
|
|
||||||
INSERT INTO resource_variants (resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetVariantsByResourceID :many
|
|
||||||
SELECT * FROM resource_variants
|
|
||||||
WHERE resource_id = $1
|
|
||||||
ORDER BY page_number ASC, variant_type ASC;
|
|
||||||
|
|
||||||
-- name: GetVariantByID :one
|
|
||||||
SELECT * FROM resource_variants
|
|
||||||
WHERE id = $1 LIMIT 1;
|
|
||||||
|
|
||||||
-- name: DeleteVariantsByResourceID :exec
|
|
||||||
DELETE FROM resource_variants WHERE resource_id = $1;
|
|
||||||
|
|
||||||
-- name: GetBestVariant :one
|
|
||||||
SELECT * FROM resource_variants
|
|
||||||
WHERE resource_id = $1 AND variant_type = $2 AND page_number = 1
|
|
||||||
LIMIT 1;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
-- name: GetResource :one
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE id = $1 LIMIT 1;
|
|
||||||
|
|
||||||
-- name: ListResources :many
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE parent_resource_id IS NULL
|
|
||||||
ORDER BY is_folder DESC, created_at DESC;
|
|
||||||
|
|
||||||
-- name: ListFolders :many
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE is_folder = true
|
|
||||||
ORDER BY created_at DESC;
|
|
||||||
|
|
||||||
-- name: ListResourcesByID :many
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE id = ANY($1::uuid[])
|
|
||||||
ORDER BY created_at DESC;
|
|
||||||
|
|
||||||
-- name: CreateResource :one
|
|
||||||
INSERT INTO resources (name, mime_type, size, checksum, owner_id, created_at, updated_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateFolder :one
|
|
||||||
INSERT INTO resources (name, is_folder, owner_id, parent_resource_id, created_at, updated_at)
|
|
||||||
VALUES ($1, true, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: ListResourcesByParentID :many
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE parent_resource_id = $1
|
|
||||||
ORDER BY is_folder DESC, created_at DESC;
|
|
||||||
|
|
||||||
-- name: MoveResources :exec
|
|
||||||
UPDATE resources
|
|
||||||
SET parent_resource_id = $1, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ANY($2::uuid[]);
|
|
||||||
|
|
||||||
-- name: UpdateResource :exec
|
|
||||||
UPDATE resources
|
|
||||||
SET name = $1, mime_type = $2, ocr_text = $3, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $4;
|
|
||||||
|
|
||||||
-- name: DeleteResource :exec
|
|
||||||
DELETE FROM resources
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: FindDuplicatesByNameSize :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, created_at FROM resources
|
|
||||||
WHERE name = $1 AND size = $2 AND is_folder = false
|
|
||||||
ORDER BY created_at DESC;
|
|
||||||
|
|
||||||
-- name: FindDuplicateByChecksum :one
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE checksum = $1 AND is_folder = false AND owner_id = $2
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: CountResourcesByOwner :one
|
|
||||||
SELECT COUNT(*) FROM resources
|
|
||||||
WHERE owner_id = $1 AND parent_resource_id IS NULL;
|
|
||||||
|
|
||||||
-- name: ListResourcesByOwner :many
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE owner_id = $1 AND parent_resource_id IS NULL
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT $2 OFFSET $3;
|
|
||||||
|
|
||||||
-- name: CountResourcesByParentAndOwner :one
|
|
||||||
SELECT COUNT(*) FROM resources
|
|
||||||
WHERE parent_resource_id = $1 AND owner_id = $2;
|
|
||||||
|
|
||||||
-- name: ListResourcesByParentAndOwner :many
|
|
||||||
SELECT * FROM resources
|
|
||||||
WHERE parent_resource_id = $1 AND owner_id = $2
|
|
||||||
ORDER BY is_folder DESC, created_at DESC
|
|
||||||
LIMIT $3 OFFSET $4;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
-- name: CreateRetentionPolicy :one
|
|
||||||
INSERT INTO retention_policies (user_id, storage_location_id, rule_type, rule_value)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetRetentionPolicy :one
|
|
||||||
SELECT * FROM retention_policies
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ListRetentionPoliciesByUser :many
|
|
||||||
SELECT * FROM retention_policies
|
|
||||||
WHERE user_id = $1;
|
|
||||||
|
|
||||||
-- name: ListRetentionPoliciesByLocation :many
|
|
||||||
SELECT * FROM retention_policies
|
|
||||||
WHERE storage_location_id = $1;
|
|
||||||
|
|
||||||
-- name: DeleteRetentionPolicy :exec
|
|
||||||
DELETE FROM retention_policies
|
|
||||||
WHERE id = $1;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
-- name: CreateStorageLocation :one
|
|
||||||
INSERT INTO storage_locations (user_id, device_name, role)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetStorageLocation :one
|
|
||||||
SELECT * FROM storage_locations
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ListStorageLocationsByUser :many
|
|
||||||
SELECT * FROM storage_locations
|
|
||||||
WHERE user_id = $1
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
|
|
||||||
-- name: GetServerStorageLocation :one
|
|
||||||
SELECT * FROM storage_locations
|
|
||||||
WHERE user_id = $1 AND role = 'server'
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: UpdateStorageLocationLastSeen :exec
|
|
||||||
UPDATE storage_locations
|
|
||||||
SET last_seen_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: DeleteStorageLocation :exec
|
|
||||||
DELETE FROM storage_locations
|
|
||||||
WHERE id = $1;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
-- name: CreateSyncQueueItem :one
|
|
||||||
INSERT INTO sync_queue (resource_id, storage_location_id, operation, status, attempts)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetSyncQueueItem :one
|
|
||||||
SELECT * FROM sync_queue
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ListPendingSyncItems :many
|
|
||||||
SELECT * FROM sync_queue
|
|
||||||
WHERE status = 'pending'
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
|
|
||||||
-- name: ListPendingSyncItemsByLocation :many
|
|
||||||
SELECT * FROM sync_queue
|
|
||||||
WHERE storage_location_id = $1 AND status = 'pending'
|
|
||||||
ORDER BY created_at ASC;
|
|
||||||
|
|
||||||
-- name: UpdateSyncQueueStatus :exec
|
|
||||||
UPDATE sync_queue
|
|
||||||
SET status = $1, attempts = $2, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $3;
|
|
||||||
|
|
||||||
-- name: DeleteSyncQueueItem :exec
|
|
||||||
DELETE FROM sync_queue
|
|
||||||
WHERE id = $1;
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
-- name: CreateTag :one
|
|
||||||
INSERT INTO tags (tag_name)
|
|
||||||
VALUES ($1)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetTag :one
|
|
||||||
SELECT * FROM tags
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetTagByName :one
|
|
||||||
SELECT * FROM tags
|
|
||||||
WHERE tag_name = $1;
|
|
||||||
|
|
||||||
-- name: ListTags :many
|
|
||||||
SELECT * FROM tags
|
|
||||||
ORDER BY tag_name ASC;
|
|
||||||
|
|
||||||
-- name: DeleteTag :exec
|
|
||||||
DELETE FROM tags
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: AddTagToResource :exec
|
|
||||||
INSERT INTO resource_tags (tag_id, resource_id)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
|
|
||||||
-- name: RemoveTagFromResource :exec
|
|
||||||
DELETE FROM resource_tags
|
|
||||||
WHERE tag_id = $1 AND resource_id = $2;
|
|
||||||
|
|
||||||
-- name: GetTagsByResourceID :many
|
|
||||||
SELECT t.* FROM tags t
|
|
||||||
JOIN resource_tags rt ON t.id = rt.tag_id
|
|
||||||
WHERE rt.resource_id = $1
|
|
||||||
ORDER BY t.tag_name ASC;
|
|
||||||
|
|
||||||
-- name: GetResourcesByTagID :many
|
|
||||||
SELECT r.* FROM resources r
|
|
||||||
JOIN resource_tags rt ON r.id = rt.resource_id
|
|
||||||
WHERE rt.tag_id = $1
|
|
||||||
ORDER BY r.created_at DESC;
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: rebac_relations.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createRebacRelation = `-- name: CreateRebacRelation :one
|
|
||||||
INSERT INTO rebac_relations (resource_id, subject_user_id, role, granted_by)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING id, resource_id, subject_user_id, role, granted_by, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateRebacRelationParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
SubjectUserID uuid.UUID `json:"subject_user_id"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
GrantedBy uuid.UUID `json:"granted_by"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateRebacRelation(ctx context.Context, arg CreateRebacRelationParams) (RebacRelation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createRebacRelation,
|
|
||||||
arg.ResourceID,
|
|
||||||
arg.SubjectUserID,
|
|
||||||
arg.Role,
|
|
||||||
arg.GrantedBy,
|
|
||||||
)
|
|
||||||
var i RebacRelation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.SubjectUserID,
|
|
||||||
&i.Role,
|
|
||||||
&i.GrantedBy,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRebacRelation = `-- name: DeleteRebacRelation :exec
|
|
||||||
DELETE FROM rebac_relations
|
|
||||||
WHERE resource_id = $1 AND subject_user_id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type DeleteRebacRelationParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
SubjectUserID uuid.UUID `json:"subject_user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteRebacRelation(ctx context.Context, arg DeleteRebacRelationParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteRebacRelation, arg.ResourceID, arg.SubjectUserID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRebacRelationsByResource = `-- name: DeleteRebacRelationsByResource :exec
|
|
||||||
DELETE FROM rebac_relations
|
|
||||||
WHERE resource_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteRebacRelationsByResource(ctx context.Context, resourceID uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteRebacRelationsByResource, resourceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRebacRelation = `-- name: GetRebacRelation :one
|
|
||||||
SELECT id, resource_id, subject_user_id, role, granted_by, created_at FROM rebac_relations
|
|
||||||
WHERE resource_id = $1 AND subject_user_id = $2
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetRebacRelationParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
SubjectUserID uuid.UUID `json:"subject_user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetRebacRelation(ctx context.Context, arg GetRebacRelationParams) (RebacRelation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getRebacRelation, arg.ResourceID, arg.SubjectUserID)
|
|
||||||
var i RebacRelation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.SubjectUserID,
|
|
||||||
&i.Role,
|
|
||||||
&i.GrantedBy,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasRebacRelation = `-- name: HasRebacRelation :one
|
|
||||||
SELECT EXISTS(
|
|
||||||
SELECT 1 FROM rebac_relations
|
|
||||||
WHERE resource_id = $1 AND subject_user_id = $2 AND role = $3
|
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
type HasRebacRelationParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
SubjectUserID uuid.UUID `json:"subject_user_id"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) HasRebacRelation(ctx context.Context, arg HasRebacRelationParams) (bool, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, hasRebacRelation, arg.ResourceID, arg.SubjectUserID, arg.Role)
|
|
||||||
var exists bool
|
|
||||||
err := row.Scan(&exists)
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listRebacRelationsByResource = `-- name: ListRebacRelationsByResource :many
|
|
||||||
SELECT id, resource_id, subject_user_id, role, granted_by, created_at FROM rebac_relations
|
|
||||||
WHERE resource_id = $1
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListRebacRelationsByResource(ctx context.Context, resourceID uuid.UUID) ([]RebacRelation, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listRebacRelationsByResource, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []RebacRelation
|
|
||||||
for rows.Next() {
|
|
||||||
var i RebacRelation
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.SubjectUserID,
|
|
||||||
&i.Role,
|
|
||||||
&i.GrantedBy,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listRebacRelationsBySubject = `-- name: ListRebacRelationsBySubject :many
|
|
||||||
SELECT id, resource_id, subject_user_id, role, granted_by, created_at FROM rebac_relations
|
|
||||||
WHERE subject_user_id = $1
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListRebacRelationsBySubject(ctx context.Context, subjectUserID uuid.UUID) ([]RebacRelation, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listRebacRelationsBySubject, subjectUserID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []RebacRelation
|
|
||||||
for rows.Next() {
|
|
||||||
var i RebacRelation
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.SubjectUserID,
|
|
||||||
&i.Role,
|
|
||||||
&i.GrantedBy,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveEffectiveRole = `-- name: ResolveEffectiveRole :one
|
|
||||||
SELECT resolve_effective_role($1, $2) AS role
|
|
||||||
`
|
|
||||||
|
|
||||||
type ResolveEffectiveRoleParams struct {
|
|
||||||
PUserID uuid.UUID `json:"p_user_id"`
|
|
||||||
PResourceID uuid.UUID `json:"p_resource_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ResolveEffectiveRole(ctx context.Context, arg ResolveEffectiveRoleParams) (string, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, resolveEffectiveRole, arg.PUserID, arg.PResourceID)
|
|
||||||
var role string
|
|
||||||
err := row.Scan(&role)
|
|
||||||
return role, err
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: resource_placements.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createPlacement = `-- name: CreatePlacement :one
|
|
||||||
INSERT INTO resource_placements (resource_id, storage_location_id, status, storage_key, synced_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING id, resource_id, storage_location_id, status, storage_key, synced_at, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreatePlacementParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
StorageKey sql.NullString `json:"storage_key"`
|
|
||||||
SyncedAt sql.NullTime `json:"synced_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreatePlacement(ctx context.Context, arg CreatePlacementParams) (ResourcePlacement, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createPlacement,
|
|
||||||
arg.ResourceID,
|
|
||||||
arg.StorageLocationID,
|
|
||||||
arg.Status,
|
|
||||||
arg.StorageKey,
|
|
||||||
arg.SyncedAt,
|
|
||||||
)
|
|
||||||
var i ResourcePlacement
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Status,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.SyncedAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePlacement = `-- name: DeletePlacement :exec
|
|
||||||
DELETE FROM resource_placements
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeletePlacement(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deletePlacement, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlacement = `-- name: GetPlacement :one
|
|
||||||
SELECT id, resource_id, storage_location_id, status, storage_key, synced_at, created_at FROM resource_placements
|
|
||||||
WHERE resource_id = $1 AND storage_location_id = $2
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetPlacementParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetPlacement(ctx context.Context, arg GetPlacementParams) (ResourcePlacement, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getPlacement, arg.ResourceID, arg.StorageLocationID)
|
|
||||||
var i ResourcePlacement
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Status,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.SyncedAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getServerPlacementByResource = `-- name: GetServerPlacementByResource :one
|
|
||||||
SELECT rp.id, rp.resource_id, rp.storage_location_id, rp.status, rp.storage_key, rp.synced_at, rp.created_at FROM resource_placements rp
|
|
||||||
JOIN storage_locations sl ON sl.id = rp.storage_location_id
|
|
||||||
WHERE rp.resource_id = $1 AND sl.role = 'server'
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetServerPlacementByResource(ctx context.Context, resourceID uuid.UUID) (ResourcePlacement, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getServerPlacementByResource, resourceID)
|
|
||||||
var i ResourcePlacement
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Status,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.SyncedAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listPlacementsByLocation = `-- name: ListPlacementsByLocation :many
|
|
||||||
SELECT id, resource_id, storage_location_id, status, storage_key, synced_at, created_at FROM resource_placements
|
|
||||||
WHERE storage_location_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListPlacementsByLocation(ctx context.Context, storageLocationID uuid.UUID) ([]ResourcePlacement, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listPlacementsByLocation, storageLocationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ResourcePlacement
|
|
||||||
for rows.Next() {
|
|
||||||
var i ResourcePlacement
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Status,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.SyncedAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listPlacementsByResource = `-- name: ListPlacementsByResource :many
|
|
||||||
SELECT id, resource_id, storage_location_id, status, storage_key, synced_at, created_at FROM resource_placements
|
|
||||||
WHERE resource_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListPlacementsByResource(ctx context.Context, resourceID uuid.UUID) ([]ResourcePlacement, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listPlacementsByResource, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ResourcePlacement
|
|
||||||
for rows.Next() {
|
|
||||||
var i ResourcePlacement
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Status,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.SyncedAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlacementStatus = `-- name: UpdatePlacementStatus :exec
|
|
||||||
UPDATE resource_placements
|
|
||||||
SET status = $1, synced_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdatePlacementStatusParams struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePlacementStatus(ctx context.Context, arg UpdatePlacementStatusParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updatePlacementStatus, arg.Status, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: resource_variants.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createResourceVariant = `-- name: CreateResourceVariant :one
|
|
||||||
INSERT INTO resource_variants (resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
||||||
RETURNING id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateResourceVariantParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
VariantType string `json:"variant_type"`
|
|
||||||
PageNumber int32 `json:"page_number"`
|
|
||||||
Width int32 `json:"width"`
|
|
||||||
Height int32 `json:"height"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
GeneratedBy string `json:"generated_by"`
|
|
||||||
StorageKey string `json:"storage_key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateResourceVariant(ctx context.Context, arg CreateResourceVariantParams) (ResourceVariant, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createResourceVariant,
|
|
||||||
arg.ResourceID,
|
|
||||||
arg.VariantType,
|
|
||||||
arg.PageNumber,
|
|
||||||
arg.Width,
|
|
||||||
arg.Height,
|
|
||||||
arg.MimeType,
|
|
||||||
arg.GeneratedBy,
|
|
||||||
arg.StorageKey,
|
|
||||||
)
|
|
||||||
var i ResourceVariant
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.VariantType,
|
|
||||||
&i.PageNumber,
|
|
||||||
&i.Width,
|
|
||||||
&i.Height,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.GeneratedBy,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteVariantsByResourceID = `-- name: DeleteVariantsByResourceID :exec
|
|
||||||
DELETE FROM resource_variants WHERE resource_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteVariantsByResourceID(ctx context.Context, resourceID uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteVariantsByResourceID, resourceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getBestVariant = `-- name: GetBestVariant :one
|
|
||||||
SELECT id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at FROM resource_variants
|
|
||||||
WHERE resource_id = $1 AND variant_type = $2 AND page_number = 1
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetBestVariantParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
VariantType string `json:"variant_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetBestVariant(ctx context.Context, arg GetBestVariantParams) (ResourceVariant, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getBestVariant, arg.ResourceID, arg.VariantType)
|
|
||||||
var i ResourceVariant
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.VariantType,
|
|
||||||
&i.PageNumber,
|
|
||||||
&i.Width,
|
|
||||||
&i.Height,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.GeneratedBy,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVariantByID = `-- name: GetVariantByID :one
|
|
||||||
SELECT id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at FROM resource_variants
|
|
||||||
WHERE id = $1 LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetVariantByID(ctx context.Context, id uuid.UUID) (ResourceVariant, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getVariantByID, id)
|
|
||||||
var i ResourceVariant
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.VariantType,
|
|
||||||
&i.PageNumber,
|
|
||||||
&i.Width,
|
|
||||||
&i.Height,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.GeneratedBy,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVariantsByResourceID = `-- name: GetVariantsByResourceID :many
|
|
||||||
SELECT id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at FROM resource_variants
|
|
||||||
WHERE resource_id = $1
|
|
||||||
ORDER BY page_number ASC, variant_type ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetVariantsByResourceID(ctx context.Context, resourceID uuid.UUID) ([]ResourceVariant, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, getVariantsByResourceID, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ResourceVariant
|
|
||||||
for rows.Next() {
|
|
||||||
var i ResourceVariant
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.VariantType,
|
|
||||||
&i.PageNumber,
|
|
||||||
&i.Width,
|
|
||||||
&i.Height,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.GeneratedBy,
|
|
||||||
&i.StorageKey,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: resources.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
const countResourcesByOwner = `-- name: CountResourcesByOwner :one
|
|
||||||
SELECT COUNT(*) FROM resources
|
|
||||||
WHERE owner_id = $1 AND parent_resource_id IS NULL
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) CountResourcesByOwner(ctx context.Context, ownerID uuid.UUID) (int64, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, countResourcesByOwner, ownerID)
|
|
||||||
var count int64
|
|
||||||
err := row.Scan(&count)
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const countResourcesByParentAndOwner = `-- name: CountResourcesByParentAndOwner :one
|
|
||||||
SELECT COUNT(*) FROM resources
|
|
||||||
WHERE parent_resource_id = $1 AND owner_id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type CountResourcesByParentAndOwnerParams struct {
|
|
||||||
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CountResourcesByParentAndOwner(ctx context.Context, arg CountResourcesByParentAndOwnerParams) (int64, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, countResourcesByParentAndOwner, arg.ParentResourceID, arg.OwnerID)
|
|
||||||
var count int64
|
|
||||||
err := row.Scan(&count)
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFolder = `-- name: CreateFolder :one
|
|
||||||
INSERT INTO resources (name, is_folder, owner_id, parent_resource_id, created_at, updated_at)
|
|
||||||
VALUES ($1, true, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateFolderParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (Resource, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createFolder, arg.Name, arg.OwnerID, arg.ParentResourceID)
|
|
||||||
var i Resource
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const createResource = `-- name: CreateResource :one
|
|
||||||
INSERT INTO resources (name, mime_type, size, checksum, owner_id, created_at, updated_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
RETURNING id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateResourceParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateResource(ctx context.Context, arg CreateResourceParams) (Resource, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createResource,
|
|
||||||
arg.Name,
|
|
||||||
arg.MimeType,
|
|
||||||
arg.Size,
|
|
||||||
arg.Checksum,
|
|
||||||
arg.OwnerID,
|
|
||||||
)
|
|
||||||
var i Resource
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteResource = `-- name: DeleteResource :exec
|
|
||||||
DELETE FROM resources
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteResource(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteResource, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const findDuplicateByChecksum = `-- name: FindDuplicateByChecksum :one
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE checksum = $1 AND is_folder = false AND owner_id = $2
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type FindDuplicateByChecksumParams struct {
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) FindDuplicateByChecksum(ctx context.Context, arg FindDuplicateByChecksumParams) (Resource, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, findDuplicateByChecksum, arg.Checksum, arg.OwnerID)
|
|
||||||
var i Resource
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const findDuplicatesByNameSize = `-- name: FindDuplicatesByNameSize :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, created_at FROM resources
|
|
||||||
WHERE name = $1 AND size = $2 AND is_folder = false
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
type FindDuplicatesByNameSizeParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FindDuplicatesByNameSizeRow struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) FindDuplicatesByNameSize(ctx context.Context, arg FindDuplicatesByNameSizeParams) ([]FindDuplicatesByNameSizeRow, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, findDuplicatesByNameSize, arg.Name, arg.Size)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []FindDuplicatesByNameSizeRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i FindDuplicatesByNameSizeRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getResource = `-- name: GetResource :one
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE id = $1 LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetResource(ctx context.Context, id uuid.UUID) (Resource, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getResource, id)
|
|
||||||
var i Resource
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listFolders = `-- name: ListFolders :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE is_folder = true
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListFolders(ctx context.Context) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listFolders)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listResources = `-- name: ListResources :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE parent_resource_id IS NULL
|
|
||||||
ORDER BY is_folder DESC, created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListResources(ctx context.Context) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listResources)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listResourcesByID = `-- name: ListResourcesByID :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE id = ANY($1::uuid[])
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListResourcesByID(ctx context.Context, dollar_1 []uuid.UUID) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listResourcesByID, pq.Array(dollar_1))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listResourcesByOwner = `-- name: ListResourcesByOwner :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE owner_id = $1 AND parent_resource_id IS NULL
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT $2 OFFSET $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type ListResourcesByOwnerParams struct {
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ListResourcesByOwner(ctx context.Context, arg ListResourcesByOwnerParams) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listResourcesByOwner, arg.OwnerID, arg.Limit, arg.Offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listResourcesByParentAndOwner = `-- name: ListResourcesByParentAndOwner :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE parent_resource_id = $1 AND owner_id = $2
|
|
||||||
ORDER BY is_folder DESC, created_at DESC
|
|
||||||
LIMIT $3 OFFSET $4
|
|
||||||
`
|
|
||||||
|
|
||||||
type ListResourcesByParentAndOwnerParams struct {
|
|
||||||
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
|
|
||||||
OwnerID uuid.UUID `json:"owner_id"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ListResourcesByParentAndOwner(ctx context.Context, arg ListResourcesByParentAndOwnerParams) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listResourcesByParentAndOwner,
|
|
||||||
arg.ParentResourceID,
|
|
||||||
arg.OwnerID,
|
|
||||||
arg.Limit,
|
|
||||||
arg.Offset,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listResourcesByParentID = `-- name: ListResourcesByParentID :many
|
|
||||||
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
|
|
||||||
WHERE parent_resource_id = $1
|
|
||||||
ORDER BY is_folder DESC, created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListResourcesByParentID(ctx context.Context, parentResourceID uuid.NullUUID) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listResourcesByParentID, parentResourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveResources = `-- name: MoveResources :exec
|
|
||||||
UPDATE resources
|
|
||||||
SET parent_resource_id = $1, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = ANY($2::uuid[])
|
|
||||||
`
|
|
||||||
|
|
||||||
type MoveResourcesParams struct {
|
|
||||||
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
|
|
||||||
Column2 []uuid.UUID `json:"column_2"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) MoveResources(ctx context.Context, arg MoveResourcesParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, moveResources, arg.ParentResourceID, pq.Array(arg.Column2))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateResource = `-- name: UpdateResource :exec
|
|
||||||
UPDATE resources
|
|
||||||
SET name = $1, mime_type = $2, ocr_text = $3, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $4
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateResourceParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
OcrText string `json:"ocr_text"`
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateResource(ctx context.Context, arg UpdateResourceParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updateResource,
|
|
||||||
arg.Name,
|
|
||||||
arg.MimeType,
|
|
||||||
arg.OcrText,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: retention_policies.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createRetentionPolicy = `-- name: CreateRetentionPolicy :one
|
|
||||||
INSERT INTO retention_policies (user_id, storage_location_id, rule_type, rule_value)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING id, user_id, storage_location_id, rule_type, rule_value, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateRetentionPolicyParams struct {
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
RuleType string `json:"rule_type"`
|
|
||||||
RuleValue json.RawMessage `json:"rule_value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateRetentionPolicy(ctx context.Context, arg CreateRetentionPolicyParams) (RetentionPolicy, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createRetentionPolicy,
|
|
||||||
arg.UserID,
|
|
||||||
arg.StorageLocationID,
|
|
||||||
arg.RuleType,
|
|
||||||
arg.RuleValue,
|
|
||||||
)
|
|
||||||
var i RetentionPolicy
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.RuleType,
|
|
||||||
&i.RuleValue,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRetentionPolicy = `-- name: DeleteRetentionPolicy :exec
|
|
||||||
DELETE FROM retention_policies
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteRetentionPolicy(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteRetentionPolicy, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRetentionPolicy = `-- name: GetRetentionPolicy :one
|
|
||||||
SELECT id, user_id, storage_location_id, rule_type, rule_value, created_at FROM retention_policies
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetRetentionPolicy(ctx context.Context, id uuid.UUID) (RetentionPolicy, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getRetentionPolicy, id)
|
|
||||||
var i RetentionPolicy
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.RuleType,
|
|
||||||
&i.RuleValue,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listRetentionPoliciesByLocation = `-- name: ListRetentionPoliciesByLocation :many
|
|
||||||
SELECT id, user_id, storage_location_id, rule_type, rule_value, created_at FROM retention_policies
|
|
||||||
WHERE storage_location_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListRetentionPoliciesByLocation(ctx context.Context, storageLocationID uuid.UUID) ([]RetentionPolicy, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listRetentionPoliciesByLocation, storageLocationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []RetentionPolicy
|
|
||||||
for rows.Next() {
|
|
||||||
var i RetentionPolicy
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.RuleType,
|
|
||||||
&i.RuleValue,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listRetentionPoliciesByUser = `-- name: ListRetentionPoliciesByUser :many
|
|
||||||
SELECT id, user_id, storage_location_id, rule_type, rule_value, created_at FROM retention_policies
|
|
||||||
WHERE user_id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListRetentionPoliciesByUser(ctx context.Context, userID uuid.UUID) ([]RetentionPolicy, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listRetentionPoliciesByUser, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []RetentionPolicy
|
|
||||||
for rows.Next() {
|
|
||||||
var i RetentionPolicy
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.RuleType,
|
|
||||||
&i.RuleValue,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: storage_locations.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createStorageLocation = `-- name: CreateStorageLocation :one
|
|
||||||
INSERT INTO storage_locations (user_id, device_name, role)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
RETURNING id, user_id, device_name, role, created_at, last_seen_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateStorageLocationParams struct {
|
|
||||||
UserID uuid.UUID `json:"user_id"`
|
|
||||||
DeviceName string `json:"device_name"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateStorageLocation(ctx context.Context, arg CreateStorageLocationParams) (StorageLocation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createStorageLocation, arg.UserID, arg.DeviceName, arg.Role)
|
|
||||||
var i StorageLocation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.DeviceName,
|
|
||||||
&i.Role,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LastSeenAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteStorageLocation = `-- name: DeleteStorageLocation :exec
|
|
||||||
DELETE FROM storage_locations
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteStorageLocation(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteStorageLocation, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getServerStorageLocation = `-- name: GetServerStorageLocation :one
|
|
||||||
SELECT id, user_id, device_name, role, created_at, last_seen_at FROM storage_locations
|
|
||||||
WHERE user_id = $1 AND role = 'server'
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetServerStorageLocation(ctx context.Context, userID uuid.UUID) (StorageLocation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getServerStorageLocation, userID)
|
|
||||||
var i StorageLocation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.DeviceName,
|
|
||||||
&i.Role,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LastSeenAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStorageLocation = `-- name: GetStorageLocation :one
|
|
||||||
SELECT id, user_id, device_name, role, created_at, last_seen_at FROM storage_locations
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetStorageLocation(ctx context.Context, id uuid.UUID) (StorageLocation, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getStorageLocation, id)
|
|
||||||
var i StorageLocation
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.DeviceName,
|
|
||||||
&i.Role,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LastSeenAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listStorageLocationsByUser = `-- name: ListStorageLocationsByUser :many
|
|
||||||
SELECT id, user_id, device_name, role, created_at, last_seen_at FROM storage_locations
|
|
||||||
WHERE user_id = $1
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListStorageLocationsByUser(ctx context.Context, userID uuid.UUID) ([]StorageLocation, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listStorageLocationsByUser, userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []StorageLocation
|
|
||||||
for rows.Next() {
|
|
||||||
var i StorageLocation
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.DeviceName,
|
|
||||||
&i.Role,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LastSeenAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateStorageLocationLastSeen = `-- name: UpdateStorageLocationLastSeen :exec
|
|
||||||
UPDATE storage_locations
|
|
||||||
SET last_seen_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) UpdateStorageLocationLastSeen(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updateStorageLocationLastSeen, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: sync_queue.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const createSyncQueueItem = `-- name: CreateSyncQueueItem :one
|
|
||||||
INSERT INTO sync_queue (resource_id, storage_location_id, operation, status, attempts)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING id, resource_id, storage_location_id, operation, status, attempts, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateSyncQueueItemParams struct {
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
StorageLocationID uuid.UUID `json:"storage_location_id"`
|
|
||||||
Operation string `json:"operation"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Attempts int32 `json:"attempts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateSyncQueueItem(ctx context.Context, arg CreateSyncQueueItemParams) (SyncQueue, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createSyncQueueItem,
|
|
||||||
arg.ResourceID,
|
|
||||||
arg.StorageLocationID,
|
|
||||||
arg.Operation,
|
|
||||||
arg.Status,
|
|
||||||
arg.Attempts,
|
|
||||||
)
|
|
||||||
var i SyncQueue
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Operation,
|
|
||||||
&i.Status,
|
|
||||||
&i.Attempts,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteSyncQueueItem = `-- name: DeleteSyncQueueItem :exec
|
|
||||||
DELETE FROM sync_queue
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSyncQueueItem(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteSyncQueueItem, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSyncQueueItem = `-- name: GetSyncQueueItem :one
|
|
||||||
SELECT id, resource_id, storage_location_id, operation, status, attempts, created_at, updated_at FROM sync_queue
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSyncQueueItem(ctx context.Context, id uuid.UUID) (SyncQueue, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getSyncQueueItem, id)
|
|
||||||
var i SyncQueue
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Operation,
|
|
||||||
&i.Status,
|
|
||||||
&i.Attempts,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const listPendingSyncItems = `-- name: ListPendingSyncItems :many
|
|
||||||
SELECT id, resource_id, storage_location_id, operation, status, attempts, created_at, updated_at FROM sync_queue
|
|
||||||
WHERE status = 'pending'
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListPendingSyncItems(ctx context.Context) ([]SyncQueue, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listPendingSyncItems)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []SyncQueue
|
|
||||||
for rows.Next() {
|
|
||||||
var i SyncQueue
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Operation,
|
|
||||||
&i.Status,
|
|
||||||
&i.Attempts,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listPendingSyncItemsByLocation = `-- name: ListPendingSyncItemsByLocation :many
|
|
||||||
SELECT id, resource_id, storage_location_id, operation, status, attempts, created_at, updated_at FROM sync_queue
|
|
||||||
WHERE storage_location_id = $1 AND status = 'pending'
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListPendingSyncItemsByLocation(ctx context.Context, storageLocationID uuid.UUID) ([]SyncQueue, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listPendingSyncItemsByLocation, storageLocationID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []SyncQueue
|
|
||||||
for rows.Next() {
|
|
||||||
var i SyncQueue
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ResourceID,
|
|
||||||
&i.StorageLocationID,
|
|
||||||
&i.Operation,
|
|
||||||
&i.Status,
|
|
||||||
&i.Attempts,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSyncQueueStatus = `-- name: UpdateSyncQueueStatus :exec
|
|
||||||
UPDATE sync_queue
|
|
||||||
SET status = $1, attempts = $2, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateSyncQueueStatusParams struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Attempts int32 `json:"attempts"`
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSyncQueueStatus(ctx context.Context, arg UpdateSyncQueueStatusParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, updateSyncQueueStatus, arg.Status, arg.Attempts, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.31.1
|
|
||||||
// source: tags.sql
|
|
||||||
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const addTagToResource = `-- name: AddTagToResource :exec
|
|
||||||
INSERT INTO resource_tags (tag_id, resource_id)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT DO NOTHING
|
|
||||||
`
|
|
||||||
|
|
||||||
type AddTagToResourceParams struct {
|
|
||||||
TagID uuid.UUID `json:"tag_id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) AddTagToResource(ctx context.Context, arg AddTagToResourceParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, addTagToResource, arg.TagID, arg.ResourceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTag = `-- name: CreateTag :one
|
|
||||||
INSERT INTO tags (tag_name)
|
|
||||||
VALUES ($1)
|
|
||||||
RETURNING id, parent_tag_id, tag_name, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) CreateTag(ctx context.Context, tagName string) (Tag, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, createTag, tagName)
|
|
||||||
var i Tag
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ParentTagID,
|
|
||||||
&i.TagName,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteTag = `-- name: DeleteTag :exec
|
|
||||||
DELETE FROM tags
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteTag(ctx context.Context, id uuid.UUID) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, deleteTag, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getResourcesByTagID = `-- name: GetResourcesByTagID :many
|
|
||||||
SELECT r.id, r.name, r.mime_type, r.size, r.checksum, r.ocr_text, r.is_folder, r.parent_resource_id, r.owner_id, r.created_at, r.updated_at FROM resources r
|
|
||||||
JOIN resource_tags rt ON r.id = rt.resource_id
|
|
||||||
WHERE rt.tag_id = $1
|
|
||||||
ORDER BY r.created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetResourcesByTagID(ctx context.Context, tagID uuid.UUID) ([]Resource, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, getResourcesByTagID, tagID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Resource
|
|
||||||
for rows.Next() {
|
|
||||||
var i Resource
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.MimeType,
|
|
||||||
&i.Size,
|
|
||||||
&i.Checksum,
|
|
||||||
&i.OcrText,
|
|
||||||
&i.IsFolder,
|
|
||||||
&i.ParentResourceID,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTag = `-- name: GetTag :one
|
|
||||||
SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetTag(ctx context.Context, id uuid.UUID) (Tag, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getTag, id)
|
|
||||||
var i Tag
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ParentTagID,
|
|
||||||
&i.TagName,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTagByName = `-- name: GetTagByName :one
|
|
||||||
SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
|
||||||
WHERE tag_name = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetTagByName(ctx context.Context, tagName string) (Tag, error) {
|
|
||||||
row := q.db.QueryRowContext(ctx, getTagByName, tagName)
|
|
||||||
var i Tag
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ParentTagID,
|
|
||||||
&i.TagName,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTagsByResourceID = `-- name: GetTagsByResourceID :many
|
|
||||||
SELECT t.id, t.parent_tag_id, t.tag_name, t.created_at, t.updated_at FROM tags t
|
|
||||||
JOIN resource_tags rt ON t.id = rt.tag_id
|
|
||||||
WHERE rt.resource_id = $1
|
|
||||||
ORDER BY t.tag_name ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetTagsByResourceID(ctx context.Context, resourceID uuid.UUID) ([]Tag, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, getTagsByResourceID, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Tag
|
|
||||||
for rows.Next() {
|
|
||||||
var i Tag
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ParentTagID,
|
|
||||||
&i.TagName,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const listTags = `-- name: ListTags :many
|
|
||||||
SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
|
||||||
ORDER BY tag_name ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListTags(ctx context.Context) ([]Tag, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listTags)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Tag
|
|
||||||
for rows.Next() {
|
|
||||||
var i Tag
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ParentTagID,
|
|
||||||
&i.TagName,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeTagFromResource = `-- name: RemoveTagFromResource :exec
|
|
||||||
DELETE FROM resource_tags
|
|
||||||
WHERE tag_id = $1 AND resource_id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type RemoveTagFromResourceParams struct {
|
|
||||||
TagID uuid.UUID `json:"tag_id"`
|
|
||||||
ResourceID uuid.UUID `json:"resource_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) RemoveTagFromResource(ctx context.Context, arg RemoveTagFromResourceParams) error {
|
|
||||||
_, err := q.db.ExecContext(ctx, removeTagFromResource, arg.TagID, arg.ResourceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/internal/auth"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
"github.com/vaultdrop/backend/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeviceHandler struct {
|
|
||||||
placement *service.PlacementService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeviceHandler) List(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
|
|
||||||
locations, err := h.placement.ListUserLocations(userID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list devices")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type deviceResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
DeviceName string `json:"device_name"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]deviceResponse, len(locations))
|
|
||||||
for i, l := range locations {
|
|
||||||
resp[i] = deviceResponse{
|
|
||||||
ID: l.ID.String(),
|
|
||||||
DeviceName: l.DeviceName,
|
|
||||||
Role: l.Role,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *DeviceHandler) Register(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
DeviceName string `json:"device_name" binding:"required"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "device_name is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loc, err := h.placement.CreateDeviceLocation(userID, body.DeviceName)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to register device")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Created(c, gin.H{
|
|
||||||
"id": loc.ID.String(),
|
|
||||||
"device_name": loc.DeviceName,
|
|
||||||
"role": loc.Role,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventHandler struct {
|
|
||||||
broker *service.EventBroker
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEventHandler(broker *service.EventBroker) *EventHandler {
|
|
||||||
return &EventHandler{broker: broker}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *EventHandler) Stream(c *gin.Context) {
|
|
||||||
c.Header("Content-Type", "text/event-stream")
|
|
||||||
c.Header("Cache-Control", "no-cache")
|
|
||||||
c.Header("Connection", "keep-alive")
|
|
||||||
|
|
||||||
ch, _ := h.broker.Subscribe(c.Request.Context())
|
|
||||||
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
|
||||||
event, ok := <-ch
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, event.Data)
|
|
||||||
return err == nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/vaultdrop/backend/internal/auth"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Handler struct {
|
|
||||||
Resource *ResourceHandler
|
|
||||||
OCR *OCRHandler
|
|
||||||
Health *HealthHandler
|
|
||||||
Auth *auth.AuthHandler
|
|
||||||
Share *ShareHandler
|
|
||||||
Device *DeviceHandler
|
|
||||||
Sync *SyncHandler
|
|
||||||
Events *EventHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(
|
|
||||||
database *sql.DB,
|
|
||||||
resourceSvc *service.ResourceService,
|
|
||||||
ocrSvc *service.OCRService,
|
|
||||||
urlSvc *service.URLService,
|
|
||||||
authHandler *auth.AuthHandler,
|
|
||||||
conversionSvc *service.ConversionService,
|
|
||||||
rebacSvc *service.RebacService,
|
|
||||||
placementSvc *service.PlacementService,
|
|
||||||
syncSvc *service.SyncService,
|
|
||||||
eventBroker *service.EventBroker,
|
|
||||||
) *Handler {
|
|
||||||
return &Handler{
|
|
||||||
Resource: &ResourceHandler{
|
|
||||||
resources: resourceSvc,
|
|
||||||
urls: urlSvc,
|
|
||||||
ocr: ocrSvc,
|
|
||||||
conversion: conversionSvc,
|
|
||||||
broker: eventBroker,
|
|
||||||
},
|
|
||||||
OCR: &OCRHandler{ocr: ocrSvc, resources: resourceSvc},
|
|
||||||
Health: &HealthHandler{db: database, ocr: ocrSvc},
|
|
||||||
Auth: authHandler,
|
|
||||||
Share: &ShareHandler{rebac: rebacSvc},
|
|
||||||
Device: &DeviceHandler{placement: placementSvc},
|
|
||||||
Sync: &SyncHandler{sync: syncSvc},
|
|
||||||
Events: NewEventHandler(eventBroker),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthHandler struct {
|
|
||||||
db *sql.DB
|
|
||||||
ocr interface{ HealthCheck() error }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HealthHandler) Check(c *gin.Context) {
|
|
||||||
checks := gin.H{}
|
|
||||||
|
|
||||||
dbErr := h.db.Ping()
|
|
||||||
if dbErr != nil {
|
|
||||||
checks["database"] = "error: " + dbErr.Error()
|
|
||||||
} else {
|
|
||||||
checks["database"] = "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
ocrErr := h.ocr.HealthCheck()
|
|
||||||
if ocrErr != nil {
|
|
||||||
checks["ocr"] = "error: " + ocrErr.Error()
|
|
||||||
} else {
|
|
||||||
checks["ocr"] = "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
status := "healthy"
|
|
||||||
for _, v := range checks {
|
|
||||||
if v != "ok" {
|
|
||||||
status = "degraded"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{
|
|
||||||
"status": status,
|
|
||||||
"checks": checks,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
resources *service.ResourceService
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1,523 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/internal/auth"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
"github.com/vaultdrop/backend/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ResourceHandler struct {
|
|
||||||
resources *service.ResourceService
|
|
||||||
urls *service.URLService
|
|
||||||
ocr *service.OCRService
|
|
||||||
conversion *service.ConversionService
|
|
||||||
broker *service.EventBroker
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) Upload(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
|
|
||||||
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.resources.Upload(file, userID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "UPLOAD_ERROR", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.ocr.Enqueue(result.ID, result.Path); err != nil {
|
|
||||||
log.Printf("WARN %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if service.IsConvertible(result.MimeType) {
|
|
||||||
if err := h.conversion.Enqueue(result.ID, result.Path, result.MimeType); err != nil {
|
|
||||||
log.Printf("WARN %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
results = append(results, gin.H{
|
|
||||||
"id": result.ID,
|
|
||||||
"name": result.Name,
|
|
||||||
})
|
|
||||||
|
|
||||||
h.broker.Publish("resource.created", result.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, results)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePagination(c *gin.Context) (page, limit int) {
|
|
||||||
page = 1
|
|
||||||
limit = 20
|
|
||||||
if p := c.Query("page"); p != "" {
|
|
||||||
if n, err := strconv.Atoi(p); err == nil && n > 0 {
|
|
||||||
page = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if l := c.Query("limit"); l != "" {
|
|
||||||
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
|
||||||
if n > 100 {
|
|
||||||
n = 100
|
|
||||||
}
|
|
||||||
limit = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) List(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
thumbnailQuality := c.Query("thumbnail")
|
|
||||||
page, limit := parsePagination(c)
|
|
||||||
|
|
||||||
resources, total, err := h.resources.List(userID, page, limit)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("ERROR List resources: %v", err)
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list resources")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type resourceResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Tags []tagResponse `json:"tags"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
OcrText string `json:"ocrText,omitempty"`
|
|
||||||
ParentID string `json:"parentResourceId,omitempty"`
|
|
||||||
IsFolder bool `json:"isFolder"`
|
|
||||||
UpdatedAt string `json:"updatedAt"`
|
|
||||||
OwnerID string `json:"ownerId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]resourceResponse, len(resources))
|
|
||||||
for i, r := range resources {
|
|
||||||
tags := []tagResponse{}
|
|
||||||
for _, tag := range r.Tags {
|
|
||||||
tags = append(tags, tagResponse{
|
|
||||||
ID: tag.ID,
|
|
||||||
TagName: tag.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadURL := h.urls.GenerateDownloadURL(r.ID)
|
|
||||||
var thumbURL string
|
|
||||||
if thumbnailQuality != "" {
|
|
||||||
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
|
|
||||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp[i] = resourceResponse{
|
|
||||||
ID: r.ID,
|
|
||||||
URL: downloadURL,
|
|
||||||
ThumbnailURL: thumbURL,
|
|
||||||
Name: r.Name,
|
|
||||||
Size: r.Size,
|
|
||||||
Tags: tags,
|
|
||||||
CreatedAt: r.CreatedAt.String(),
|
|
||||||
ParentID: r.ParentResourceID,
|
|
||||||
OcrText: r.OcrText,
|
|
||||||
IsFolder: r.IsFolder,
|
|
||||||
UpdatedAt: r.UpdatedAt.String(),
|
|
||||||
MimeType: r.MimeType,
|
|
||||||
OwnerID: r.OwnerID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Paginated(c, resp, page, total)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) 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.resources.GetStoragePath(id)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.File(path.Clean(storagePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) Get(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
thumbnailQuality := c.Query("thumbnail")
|
|
||||||
|
|
||||||
resource, err := h.resources.Get(id)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type variantResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
PageNumber int `json:"pageNumber"`
|
|
||||||
VariantType string `json:"variantType"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := []tagResponse{}
|
|
||||||
for _, tag := range resource.Tags {
|
|
||||||
tags = append(tags, tagResponse{
|
|
||||||
ID: tag.ID,
|
|
||||||
TagName: tag.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadURL := h.urls.GenerateDownloadURL(resource.ID)
|
|
||||||
var thumbURL string
|
|
||||||
if thumbnailQuality != "" {
|
|
||||||
if best := h.resources.GetBestVariant(resource.ID, thumbnailQuality); best != nil {
|
|
||||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dbVariants, _ := h.resources.GetVariantsByResourceID(resource.ID)
|
|
||||||
variants := make([]variantResponse, len(dbVariants))
|
|
||||||
for i, v := range dbVariants {
|
|
||||||
variants[i] = variantResponse{
|
|
||||||
ID: v.ID,
|
|
||||||
PageNumber: v.PageNumber,
|
|
||||||
VariantType: v.VariantType,
|
|
||||||
Width: v.Width,
|
|
||||||
Height: v.Height,
|
|
||||||
URL: h.urls.GenerateVariantURL(v.ID),
|
|
||||||
MimeType: v.MimeType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{
|
|
||||||
"id": resource.ID,
|
|
||||||
"name": resource.Name,
|
|
||||||
"url": downloadURL,
|
|
||||||
"thumbnailUrl": thumbURL,
|
|
||||||
"size": resource.Size,
|
|
||||||
"mimeType": resource.MimeType,
|
|
||||||
"tags": tags,
|
|
||||||
"createdAt": resource.CreatedAt,
|
|
||||||
"updatedAt": resource.UpdatedAt,
|
|
||||||
"ocrText": resource.OcrText,
|
|
||||||
"isFolder": resource.IsFolder,
|
|
||||||
"parentResourceId": resource.ParentResourceID,
|
|
||||||
"ownerId": resource.OwnerID,
|
|
||||||
"variants": variants,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) Delete(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
result, err := h.resources.DeleteRecursive(id, userID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, service.ErrForbidden) {
|
|
||||||
api.Error(c, http.StatusForbidden, "FORBIDDEN", "You do not own this resource")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete resource")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range result.StoragePaths {
|
|
||||||
os.Remove(path.Clean(p))
|
|
||||||
}
|
|
||||||
for _, v := range result.Variants {
|
|
||||||
os.Remove(path.Clean(v.StorageKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{"deleted": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) AddTags(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
Tags []string `json:"tags" binding:"required"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.resources.AddTags(id, body.Tags); err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tags, err := h.resources.GetTagsByResourceID(id)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) GetTags(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
tags, err := h.resources.GetTagsByResourceID(id)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api.Success(c, tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) MoveResources(c *gin.Context) {
|
|
||||||
var body struct {
|
|
||||||
ResourceIDs []string `json:"resource_ids" binding:"required"`
|
|
||||||
ParentResourceID *string `json:"parent_resource_id"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'resource_ids' array")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.resources.MoveResources(body.ResourceIDs, body.ParentResourceID); err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to move resources")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{"moved": len(body.ResourceIDs)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) ListFolders(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
folders, err := h.resources.ListFolders(userID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list folders")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api.Success(c, folders)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) CreateFolder(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
ParentResourceID *string `json:"parent_resource_id"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'name'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
folder, err := h.resources.CreateFolder(body.Name, userID, body.ParentResourceID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to create folder")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) ListByParent(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
parentID := c.Param("id")
|
|
||||||
thumbnailQuality := c.Query("thumbnail")
|
|
||||||
page, limit := parsePagination(c)
|
|
||||||
|
|
||||||
resources, total, err := h.resources.ListResourcesByParentID(parentID, userID, page, limit)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list resources in folder")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type resourceResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Tags []tagResponse `json:"tags"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
OcrText string `json:"ocrText,omitempty"`
|
|
||||||
ParentID string `json:"parentResourceId,omitempty"`
|
|
||||||
IsFolder bool `json:"isFolder"`
|
|
||||||
UpdatedAt string `json:"updatedAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]resourceResponse, len(resources))
|
|
||||||
for i, r := range resources {
|
|
||||||
tags := []tagResponse{}
|
|
||||||
for _, tag := range r.Tags {
|
|
||||||
tags = append(tags, tagResponse{
|
|
||||||
ID: tag.ID,
|
|
||||||
TagName: tag.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadURL := h.urls.GenerateDownloadURL(r.ID)
|
|
||||||
var thumbURL string
|
|
||||||
if thumbnailQuality != "" {
|
|
||||||
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
|
|
||||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp[i] = resourceResponse{
|
|
||||||
ID: r.ID,
|
|
||||||
URL: downloadURL,
|
|
||||||
ThumbnailURL: thumbURL,
|
|
||||||
Name: r.Name,
|
|
||||||
Size: r.Size,
|
|
||||||
Tags: tags,
|
|
||||||
CreatedAt: r.CreatedAt.String(),
|
|
||||||
ParentID: r.ParentResourceID,
|
|
||||||
OcrText: r.OcrText,
|
|
||||||
IsFolder: r.IsFolder,
|
|
||||||
UpdatedAt: r.UpdatedAt.String(),
|
|
||||||
MimeType: r.MimeType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Paginated(c, resp, page, total)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) GetVariants(c *gin.Context) {
|
|
||||||
id := c.Param("id")
|
|
||||||
variants, err := h.resources.GetVariantsByResourceID(id)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch variants")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type variantResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
PageNumber int `json:"pageNumber"`
|
|
||||||
VariantType string `json:"variantType"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]variantResponse, len(variants))
|
|
||||||
for i, v := range variants {
|
|
||||||
resp[i] = variantResponse{
|
|
||||||
ID: v.ID,
|
|
||||||
PageNumber: v.PageNumber,
|
|
||||||
VariantType: v.VariantType,
|
|
||||||
Width: v.Width,
|
|
||||||
Height: v.Height,
|
|
||||||
URL: h.urls.GenerateVariantURL(v.ID),
|
|
||||||
MimeType: v.MimeType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) ServeVariant(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.resources.GetVariantStoragePath(id)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusNotFound, "VARIANT_NOT_FOUND", "Variant not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.File(path.Clean(storagePath))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ResourceHandler) CheckDuplicates(c *gin.Context) {
|
|
||||||
var body struct {
|
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Size int64 `json:"size" binding:"required"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'name' and 'size'")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
duplicates, err := h.resources.FindDuplicatesByNameSize(body.Name, body.Size)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to check duplicates")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type dupResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Checksum string `json:"checksum"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]dupResponse, len(duplicates))
|
|
||||||
for i, d := range duplicates {
|
|
||||||
resp[i] = dupResponse{
|
|
||||||
ID: d.ID.String(),
|
|
||||||
Name: d.Name,
|
|
||||||
MimeType: d.MimeType,
|
|
||||||
Size: d.Size,
|
|
||||||
Checksum: d.Checksum,
|
|
||||||
CreatedAt: d.CreatedAt.Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{
|
|
||||||
"duplicates": resp,
|
|
||||||
"count": len(resp),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) {
|
|
||||||
api := r.Group("/api/v1")
|
|
||||||
|
|
||||||
// Public
|
|
||||||
api.GET("/health", h.Health.Check)
|
|
||||||
api.POST("/auth/register", h.Auth.Register)
|
|
||||||
api.POST("/auth/login", h.Auth.Login)
|
|
||||||
api.POST("/auth/refresh", h.Auth.Refresh)
|
|
||||||
api.POST("/auth/logout", h.Auth.Logout)
|
|
||||||
api.GET("/resources/download/:id", h.Resource.Download)
|
|
||||||
api.GET("/variants/:id", h.Resource.ServeVariant)
|
|
||||||
|
|
||||||
// Protected
|
|
||||||
protected := api.Group("")
|
|
||||||
protected.Use(authMiddleware.RequireAuth())
|
|
||||||
|
|
||||||
// Resources
|
|
||||||
protected.GET("/resources", h.Resource.List)
|
|
||||||
protected.POST("/resources/upload", h.Resource.Upload)
|
|
||||||
protected.POST("/resources/move", h.Resource.MoveResources)
|
|
||||||
protected.POST("/resources/folders", h.Resource.CreateFolder)
|
|
||||||
protected.GET("/resources/folders", h.Resource.ListFolders)
|
|
||||||
protected.GET("/resources/folders/:id/resources", h.Resource.ListByParent)
|
|
||||||
protected.DELETE("/resources/:id", h.Resource.Delete)
|
|
||||||
protected.GET("/resources/:id", h.Resource.Get)
|
|
||||||
|
|
||||||
// Tags
|
|
||||||
protected.POST("/resources/:id/tags", h.Resource.AddTags)
|
|
||||||
protected.GET("/resources/:id/tags", h.Resource.GetTags)
|
|
||||||
|
|
||||||
// Variants
|
|
||||||
protected.GET("/resources/:id/variants", h.Resource.GetVariants)
|
|
||||||
|
|
||||||
// Events (SSE)
|
|
||||||
protected.GET("/events", h.Events.Stream)
|
|
||||||
|
|
||||||
// Dedup
|
|
||||||
protected.POST("/resources/dedup-check", h.Resource.CheckDuplicates)
|
|
||||||
|
|
||||||
// Sharing (ReBAC)
|
|
||||||
protected.POST("/resources/:id/share", h.Share.Grant)
|
|
||||||
protected.DELETE("/resources/:id/share/:userId", h.Share.Revoke)
|
|
||||||
protected.GET("/resources/:id/share", h.Share.List)
|
|
||||||
protected.GET("/resources/:id/access", h.Share.Check)
|
|
||||||
|
|
||||||
// Devices
|
|
||||||
protected.GET("/devices", h.Device.List)
|
|
||||||
protected.POST("/devices", h.Device.Register)
|
|
||||||
|
|
||||||
// Sync
|
|
||||||
protected.POST("/sync/pull", h.Sync.Pull)
|
|
||||||
protected.POST("/sync/push", h.Sync.Push)
|
|
||||||
|
|
||||||
// OCR
|
|
||||||
protected.POST("/ocr/jobs", h.OCR.CreateJob)
|
|
||||||
protected.GET("/ocr/jobs/:id", h.OCR.GetJobStatus)
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/internal/auth"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
"github.com/vaultdrop/backend/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ShareHandler struct {
|
|
||||||
rebac *service.RebacService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ShareHandler) Grant(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
resourceID := c.Param("id")
|
|
||||||
|
|
||||||
var body struct {
|
|
||||||
SubjectUserID string `json:"subject_user_id" binding:"required"`
|
|
||||||
Role string `json:"role" binding:"required"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "subject_user_id and role are required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.rebac.GrantRole(userID, resourceID, body.SubjectUserID, body.Role); err != nil {
|
|
||||||
api.Error(c, http.StatusForbidden, "FORBIDDEN", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{"granted": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ShareHandler) Revoke(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
resourceID := c.Param("id")
|
|
||||||
subjectID := c.Param("userId")
|
|
||||||
|
|
||||||
if err := h.rebac.RevokeRole(userID, resourceID, subjectID); err != nil {
|
|
||||||
api.Error(c, http.StatusForbidden, "FORBIDDEN", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{"revoked": true})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ShareHandler) List(c *gin.Context) {
|
|
||||||
resourceID := c.Param("id")
|
|
||||||
|
|
||||||
relations, err := h.rebac.ListShares(resourceID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list shares")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type shareResponse struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := make([]shareResponse, len(relations))
|
|
||||||
for i, r := range relations {
|
|
||||||
resp[i] = shareResponse{
|
|
||||||
UserID: r.SubjectUserID.String(),
|
|
||||||
Role: r.Role,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ShareHandler) Check(c *gin.Context) {
|
|
||||||
userID := c.GetString(auth.UserIDKey)
|
|
||||||
resourceID := c.Param("id")
|
|
||||||
|
|
||||||
role, err := h.rebac.ResolveEffectiveRole(userID, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to resolve role")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, gin.H{
|
|
||||||
"role": role,
|
|
||||||
"access": role != "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/vaultdrop/backend/internal/service"
|
|
||||||
"github.com/vaultdrop/backend/pkg/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SyncHandler struct {
|
|
||||||
sync *service.SyncService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SyncHandler) Pull(c *gin.Context) {
|
|
||||||
var body struct {
|
|
||||||
LocationID string `json:"location_id"`
|
|
||||||
}
|
|
||||||
c.ShouldBindJSON(&body)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
var items interface{}
|
|
||||||
if body.LocationID != "" {
|
|
||||||
items, err = h.sync.ListPending(body.LocationID)
|
|
||||||
} else {
|
|
||||||
items, err = h.sync.ListAllPending()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Success(c, items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *SyncHandler) Push(c *gin.Context) {
|
|
||||||
var body struct {
|
|
||||||
LocationID string `json:"location_id" binding:"required"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "location_id is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
items, err := h.sync.ListPending(body.LocationID)
|
|
||||||
if err != nil {
|
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Created(c, gin.H{
|
|
||||||
"pending": len(items),
|
|
||||||
"message": "Push initiated",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type Tag struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resource struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
OcrText string `json:"ocrText,omitempty"`
|
|
||||||
Tags []Tag `json:"tags"`
|
|
||||||
IsFolder bool `json:"isFolder"`
|
|
||||||
ParentResourceID string `json:"parentResourceId,omitempty"`
|
|
||||||
OwnerID string `json:"ownerId"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Variant struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
ResourceID string `json:"resourceId"`
|
|
||||||
VariantType string `json:"variantType"`
|
|
||||||
PageNumber int `json:"pageNumber"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
StorageKey string `json:"-"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
GeneratedBy string `json:"generatedBy"`
|
|
||||||
CreatedAt string `json:"createdAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UploadResult struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
MimeType string `json:"mimeType"`
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package ocr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ledongthuc/pdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
endpoint string
|
|
||||||
httpClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(endpoint string) *Client {
|
|
||||||
return &Client{
|
|
||||||
endpoint: endpoint,
|
|
||||||
httpClient: &http.Client{
|
|
||||||
Timeout: 120 * time.Second,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Recognize(imageData []byte) ([]TextBlock, error) {
|
|
||||||
|
|
||||||
docType := DetectDocumentType(imageData)
|
|
||||||
|
|
||||||
switch docType {
|
|
||||||
case PDFScanned:
|
|
||||||
fmt.Println("PDFScanned -> PaddleOCR")
|
|
||||||
case PDFText:
|
|
||||||
fmt.Println("PDFText -> PaddleOCR")
|
|
||||||
case Image:
|
|
||||||
fmt.Println("Image -> PaddleOCR")
|
|
||||||
default:
|
|
||||||
return []TextBlock{}, fmt.Errorf("DOCTYPE not supported => %s", docType)
|
|
||||||
}
|
|
||||||
|
|
||||||
b64 := base64.StdEncoding.EncodeToString(imageData)
|
|
||||||
|
|
||||||
reqBody, err := json.Marshal(OCRRequest{Image: b64})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.httpClient.Post(
|
|
||||||
c.endpoint+"/ocr",
|
|
||||||
"application/json",
|
|
||||||
bytes.NewReader(reqBody),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("call paddleocr: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("paddleocr returned %d: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
var ocrResp OCRResponse
|
|
||||||
if err := json.Unmarshal(body, &ocrResp); err != nil {
|
|
||||||
return nil, fmt.Errorf("decode response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ocrResp.ErrorCode != 0 {
|
|
||||||
return nil, fmt.Errorf("paddleocr error %d: %s", ocrResp.ErrorCode, ocrResp.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return flattenResults(ocrResp.Result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type DocType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Image DocType = "image"
|
|
||||||
PDFText DocType = "pdf_text"
|
|
||||||
PDFScanned DocType = "pdf_scanned"
|
|
||||||
Unknown DocType = "unknown"
|
|
||||||
)
|
|
||||||
|
|
||||||
func isPDFText(data []byte) bool {
|
|
||||||
reader := bytes.NewReader(data)
|
|
||||||
|
|
||||||
r, err := pdf.NewReader(reader, int64(len(data)))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 1; i <= r.NumPage(); i++ {
|
|
||||||
page := r.Page(i)
|
|
||||||
if page.V.IsNull() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
text, _ := page.GetPlainText(nil)
|
|
||||||
if len(text) > 20 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func DetectDocumentType(data []byte) DocType {
|
|
||||||
mime := http.DetectContentType(data)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case mime == "application/pdf":
|
|
||||||
if isPDFText(data) {
|
|
||||||
return PDFText
|
|
||||||
}
|
|
||||||
return PDFScanned
|
|
||||||
|
|
||||||
case bytes.HasPrefix(data, []byte{0xFF, 0xD8}): // JPEG
|
|
||||||
return Image
|
|
||||||
|
|
||||||
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47}): // PNG
|
|
||||||
return Image
|
|
||||||
|
|
||||||
default:
|
|
||||||
return Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) HealthCheck() error {
|
|
||||||
resp, err := c.httpClient.Get(c.endpoint + "/health")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("health check failed: status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func flattenResults(result OCRResult) []TextBlock {
|
|
||||||
var blocks []TextBlock
|
|
||||||
for _, page := range result.OCRResults {
|
|
||||||
for i, text := range page.RecTexts {
|
|
||||||
score := 0.0
|
|
||||||
if i < len(page.RecScores) {
|
|
||||||
score = page.RecScores[i]
|
|
||||||
}
|
|
||||||
blocks = append(blocks, TextBlock{Text: text, Score: score})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blocks
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package ocr
|
|
||||||
|
|
||||||
type OCRRequest struct {
|
|
||||||
Image string `json:"image"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OCRResponse struct {
|
|
||||||
ErrorCode int `json:"errorCode"`
|
|
||||||
Result OCRResult `json:"result"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OCRResult struct {
|
|
||||||
OCRResults []OCRPageResult `json:"ocrResults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OCRPageResult struct {
|
|
||||||
RecTexts []string `json:"rec_texts"`
|
|
||||||
RecScores []float64 `json:"rec_scores"`
|
|
||||||
RecBoxes [][]int `json:"rec_boxes"`
|
|
||||||
RecPolys [][][]int `json:"rec_polys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextBlock struct {
|
|
||||||
Text string `json:"text"`
|
|
||||||
Score float64 `json:"score"`
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateSHA256Hash(data []byte) []byte {
|
|
||||||
h := sha256.Sum256(data)
|
|
||||||
return h[:]
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
_ "image/jpeg"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConversionJob struct {
|
|
||||||
ResourceID string
|
|
||||||
FilePath string
|
|
||||||
MimeType string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConversionService struct {
|
|
||||||
queries *db.Queries
|
|
||||||
cfg *config.Config
|
|
||||||
jobs chan ConversionJob
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConversionService(queries *db.Queries, cfg *config.Config) *ConversionService {
|
|
||||||
return &ConversionService{
|
|
||||||
queries: queries,
|
|
||||||
cfg: cfg,
|
|
||||||
jobs: make(chan ConversionJob, 100),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) Start(workerCount int) {
|
|
||||||
for i := range workerCount {
|
|
||||||
go s.worker()
|
|
||||||
log.Printf("[Conversion] Worker %d started", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) Stop() {
|
|
||||||
close(s.jobs)
|
|
||||||
log.Println("[Conversion] Worker stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) Enqueue(resourceID, filePath, mimeType string) error {
|
|
||||||
select {
|
|
||||||
case s.jobs <- ConversionJob{ResourceID: resourceID, FilePath: filePath, MimeType: mimeType}:
|
|
||||||
log.Printf("[Conversion] Enqueued resource %s", resourceID)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
log.Printf("[Conversion] Queue full, dropping resource %s", resourceID)
|
|
||||||
return fmt.Errorf("conversion queue full (%d pending)", len(s.jobs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) worker() {
|
|
||||||
for job := range s.jobs {
|
|
||||||
s.process(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) process(job ConversionJob) {
|
|
||||||
log.Printf("[Conversion] Processing resource %s (mime: %s)", job.ResourceID, job.MimeType)
|
|
||||||
|
|
||||||
pdfPath := job.FilePath
|
|
||||||
tmpDir := ""
|
|
||||||
|
|
||||||
if isOfficeDocument(job.MimeType) {
|
|
||||||
var err error
|
|
||||||
pdfPath, tmpDir, err = s.convertToPDF(job.FilePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Conversion] Failed to convert resource %s to PDF: %v", job.ResourceID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
} else if !isPDF(job.MimeType) {
|
|
||||||
log.Printf("[Conversion] Skipping resource %s: unsupported mime type %s", job.ResourceID, job.MimeType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbDir := filepath.Join(s.cfg.ThumbnailDir, job.ResourceID)
|
|
||||||
if err := os.MkdirAll(thumbDir, 0o755); err != nil {
|
|
||||||
log.Printf("[Conversion] Failed to create thumbnail dir for %s: %v", job.ResourceID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolutions := []struct {
|
|
||||||
label string
|
|
||||||
dpi int
|
|
||||||
}{
|
|
||||||
{"thumbnail_small", 21},
|
|
||||||
{"thumbnail_full", 200},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, res := range resolutions {
|
|
||||||
pages, err := s.convertPDFToImages(pdfPath, thumbDir, res.dpi)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Conversion] Failed to convert resource %s to images (res=%s): %v", job.ResourceID, res.label, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, page := range pages {
|
|
||||||
width, height, err := getImageDimensions(page.path)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Conversion] Failed to get dimensions for %s: %v", page.path, err)
|
|
||||||
width, height = 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
dstPath := filepath.Join(thumbDir, uuid.New().String()+".jpg")
|
|
||||||
if err := os.Rename(page.path, dstPath); err != nil {
|
|
||||||
log.Printf("[Conversion] Failed to move %s to %s: %v", page.path, dstPath, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceUUID, _ := uuid.Parse(job.ResourceID)
|
|
||||||
_, err = s.queries.CreateResourceVariant(context.Background(), db.CreateResourceVariantParams{
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
VariantType: res.label,
|
|
||||||
PageNumber: int32(page.number),
|
|
||||||
Width: int32(width),
|
|
||||||
Height: int32(height),
|
|
||||||
MimeType: "image/jpeg",
|
|
||||||
GeneratedBy: "server",
|
|
||||||
StorageKey: dstPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[Conversion] Failed to create variant record for resource %s page %d: %v", job.ResourceID, page.number, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[Conversion] Generated %d %s images for resource %s", len(pages), res.label, job.ResourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[Conversion] Completed resource %s", job.ResourceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) convertToPDF(inputPath string) (string, string, error) {
|
|
||||||
tmpDir, err := os.MkdirTemp("", "conversion-*")
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("create temp dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(s.cfg.LibreOfficePath,
|
|
||||||
"--headless",
|
|
||||||
"--convert-to", "pdf",
|
|
||||||
"--outdir", tmpDir,
|
|
||||||
inputPath,
|
|
||||||
)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(tmpDir)
|
|
||||||
return "", "", fmt.Errorf("libreoffice conversion failed: %s: %w", string(output), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseName := filepath.Base(inputPath)
|
|
||||||
pdfName := strings.TrimSuffix(baseName, filepath.Ext(baseName)) + ".pdf"
|
|
||||||
pdfPath := filepath.Join(tmpDir, pdfName)
|
|
||||||
|
|
||||||
if _, err := os.Stat(pdfPath); os.IsNotExist(err) {
|
|
||||||
os.RemoveAll(tmpDir)
|
|
||||||
return "", "", fmt.Errorf("PDF not found at %s", pdfPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pdfPath, tmpDir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type imagePage struct {
|
|
||||||
number int
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ConversionService) convertPDFToImages(pdfPath, outputDir string, dpi int) ([]imagePage, error) {
|
|
||||||
prefix := filepath.Join(outputDir, fmt.Sprintf("tmp_%d_", dpi))
|
|
||||||
|
|
||||||
cmd := exec.Command(s.cfg.PdftoppmPath,
|
|
||||||
"-jpeg",
|
|
||||||
"-r", fmt.Sprintf("%d", dpi),
|
|
||||||
pdfPath,
|
|
||||||
prefix,
|
|
||||||
)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("pdftoppm failed: %s: %w", string(output), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(outputDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read output dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pages []imagePage
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := entry.Name()
|
|
||||||
if !strings.HasPrefix(name, fmt.Sprintf("tmp_%d_", dpi)) || !strings.HasSuffix(name, ".jpg") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
raw := strings.TrimPrefix(name, fmt.Sprintf("tmp_%d_", dpi))
|
|
||||||
raw = strings.TrimPrefix(raw, "-")
|
|
||||||
var num int
|
|
||||||
if _, err := fmt.Sscanf(raw, "%d", &num); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pages = append(pages, imagePage{
|
|
||||||
number: num,
|
|
||||||
path: filepath.Join(outputDir, name),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPDF(mimeType string) bool {
|
|
||||||
return strings.Contains(mimeType, "pdf")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOfficeDocument(mimeType string) bool {
|
|
||||||
officeTypes := []string{
|
|
||||||
"application/vnd.openxmlformats-officedocument",
|
|
||||||
"application/vnd.ms-excel",
|
|
||||||
"application/msword",
|
|
||||||
"application/vnd.ms-powerpoint",
|
|
||||||
"application/vnd.oasis.opendocument",
|
|
||||||
"application/x-doc",
|
|
||||||
"application/x-xls",
|
|
||||||
"application/x-ppt",
|
|
||||||
}
|
|
||||||
for _, t := range officeTypes {
|
|
||||||
if strings.Contains(mimeType, t) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsConvertible(mimeType string) bool {
|
|
||||||
return isPDF(mimeType) || isOfficeDocument(mimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getImageDimensions(path string) (int, int, error) {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
cfg, _, err := image.DecodeConfig(f)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg.Width, cfg.Height, nil
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Type string
|
|
||||||
Data string
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventBroker struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
subscribers map[string]chan Event
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEventBroker() *EventBroker {
|
|
||||||
return &EventBroker{
|
|
||||||
subscribers: make(map[string]chan Event),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *EventBroker) Subscribe(ctx context.Context) (<-chan Event, string) {
|
|
||||||
id := uuid.New().String()
|
|
||||||
ch := make(chan Event, 16)
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
b.subscribers[id] = ch
|
|
||||||
b.mu.Unlock()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
b.mu.Lock()
|
|
||||||
delete(b.subscribers, id)
|
|
||||||
close(ch)
|
|
||||||
b.mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ch, id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *EventBroker) Publish(eventType, data string) {
|
|
||||||
b.mu.RLock()
|
|
||||||
defer b.mu.RUnlock()
|
|
||||||
for _, ch := range b.subscribers {
|
|
||||||
select {
|
|
||||||
case ch <- Event{Type: eventType, Data: data}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
"github.com/vaultdrop/backend/internal/ocr"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OCRJob struct {
|
|
||||||
DBID uuid.UUID
|
|
||||||
ResourceID string
|
|
||||||
FilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OCRService struct {
|
|
||||||
client *ocr.Client
|
|
||||||
resourceSvc *ResourceService
|
|
||||||
broker *EventBroker
|
|
||||||
queries *db.Queries
|
|
||||||
jobs chan OCRJob
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOCRService(database *sql.DB, queries *db.Queries, cfg *config.Config, resourceSvc *ResourceService, broker *EventBroker) *OCRService {
|
|
||||||
return &OCRService{
|
|
||||||
client: ocr.NewClient(cfg.OCREndpoint),
|
|
||||||
resourceSvc: resourceSvc,
|
|
||||||
broker: broker,
|
|
||||||
queries: queries,
|
|
||||||
jobs: make(chan OCRJob, 100),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) Start(workerCount int) {
|
|
||||||
s.replenish()
|
|
||||||
for i := range workerCount {
|
|
||||||
go s.worker()
|
|
||||||
log.Printf("[OCR] Worker %d started", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) Stop() {
|
|
||||||
close(s.jobs)
|
|
||||||
log.Println("[OCR] Worker stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) Enqueue(resourceID, filePath string) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
resourceUUID, err := uuid.Parse(resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse resource id: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbJob, err := s.queries.CreateOCRJob(ctx, db.CreateOCRJobParams{
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
FilePath: filePath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create ocr job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
job := OCRJob{DBID: dbJob.ID, ResourceID: resourceID, FilePath: filePath}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case s.jobs <- job:
|
|
||||||
log.Printf("[OCR] Enqueued resource %s (job %s)", resourceID, dbJob.ID)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
log.Printf("[OCR] Queue full, resource %s persisted as pending (job %s)", resourceID, dbJob.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) replenish() {
|
|
||||||
ctx := context.Background()
|
|
||||||
pending, err := s.queries.ListPendingOCRJobs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[OCR] Failed to load pending jobs: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, j := range pending {
|
|
||||||
job := OCRJob{DBID: j.ID, ResourceID: j.ResourceID.String(), FilePath: j.FilePath}
|
|
||||||
select {
|
|
||||||
case s.jobs <- job:
|
|
||||||
log.Printf("[OCR] Replenished job %s (resource %s)", j.ID, j.ResourceID)
|
|
||||||
default:
|
|
||||||
log.Printf("[OCR] Queue full, leaving job %s in pending", j.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) worker() {
|
|
||||||
for job := range s.jobs {
|
|
||||||
s.process(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) process(job OCRJob) {
|
|
||||||
ctx := context.Background()
|
|
||||||
log.Printf("[OCR] Processing resource %s", job.ResourceID)
|
|
||||||
|
|
||||||
s.queries.UpdateOCRJobStatus(ctx, db.UpdateOCRJobStatusParams{
|
|
||||||
ID: job.DBID,
|
|
||||||
Status: "processing",
|
|
||||||
})
|
|
||||||
|
|
||||||
data, err := os.ReadFile(job.FilePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[OCR] Failed to read resource %s: %v", job.ResourceID, err)
|
|
||||||
s.queries.UpdateOCRJobStatus(ctx, db.UpdateOCRJobStatusParams{
|
|
||||||
ID: job.DBID,
|
|
||||||
Status: "failed",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks, err := s.client.Recognize(data)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[OCR] Failed to recognize resource %s: %v", job.ResourceID, err)
|
|
||||||
s.queries.UpdateOCRJobStatus(ctx, db.UpdateOCRJobStatusParams{
|
|
||||||
ID: job.DBID,
|
|
||||||
Status: "failed",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
text := s.FlattenResults(blocks)
|
|
||||||
|
|
||||||
if err := s.resourceSvc.UpdateOCRText(job.ResourceID, text); err != nil {
|
|
||||||
log.Printf("[OCR] Failed to update ocr_text for resource %s: %v", job.ResourceID, err)
|
|
||||||
s.queries.UpdateOCRJobStatus(ctx, db.UpdateOCRJobStatusParams{
|
|
||||||
ID: job.DBID,
|
|
||||||
Status: "failed",
|
|
||||||
ErrorMessage: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.queries.UpdateOCRJobStatus(ctx, db.UpdateOCRJobStatusParams{
|
|
||||||
ID: job.DBID,
|
|
||||||
Status: "done",
|
|
||||||
})
|
|
||||||
s.broker.Publish("ocr_done", job.ResourceID)
|
|
||||||
log.Printf("[OCR] Completed resource %s (%d chars)", job.ResourceID, len(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *OCRService) QueueLength() int {
|
|
||||||
return len(s.jobs)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PlacementService struct {
|
|
||||||
queries *db.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlacementService(queries *db.Queries) *PlacementService {
|
|
||||||
return &PlacementService{queries: queries}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlacementService) GetPlacementsForResource(resourceID string) ([]db.ResourcePlacement, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
return s.queries.ListPlacementsByResource(context.Background(), resourceUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlacementService) GetPlacementsForLocation(locationID string) ([]db.ResourcePlacement, error) {
|
|
||||||
locationUUID, _ := uuid.Parse(locationID)
|
|
||||||
return s.queries.ListPlacementsByLocation(context.Background(), locationUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlacementService) UpdatePlacementStatus(placementID, status string) error {
|
|
||||||
placementUUID, _ := uuid.Parse(placementID)
|
|
||||||
return s.queries.UpdatePlacementStatus(context.Background(), db.UpdatePlacementStatusParams{
|
|
||||||
Status: status,
|
|
||||||
ID: placementUUID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlacementService) DeletePlacement(placementID string) error {
|
|
||||||
placementUUID, _ := uuid.Parse(placementID)
|
|
||||||
return s.queries.DeletePlacement(context.Background(), placementUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlacementService) CreateDeviceLocation(userID, deviceName string) (db.StorageLocation, error) {
|
|
||||||
userUUID, _ := uuid.Parse(userID)
|
|
||||||
return s.queries.CreateStorageLocation(context.Background(), db.CreateStorageLocationParams{
|
|
||||||
UserID: userUUID,
|
|
||||||
DeviceName: deviceName,
|
|
||||||
Role: "device",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PlacementService) ListUserLocations(userID string) ([]db.StorageLocation, error) {
|
|
||||||
userUUID, _ := uuid.Parse(userID)
|
|
||||||
return s.queries.ListStorageLocationsByUser(context.Background(), userUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RebacService struct {
|
|
||||||
queries *db.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRebacService(queries *db.Queries) *RebacService {
|
|
||||||
return &RebacService{queries: queries}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RebacService) ResolveEffectiveRole(userID, resourceID string) (string, error) {
|
|
||||||
userUUID, _ := uuid.Parse(userID)
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
role, err := s.queries.ResolveEffectiveRole(context.Background(), db.ResolveEffectiveRoleParams{
|
|
||||||
PUserID: userUUID,
|
|
||||||
PResourceID: resourceUUID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("resolve effective role: %w", err)
|
|
||||||
}
|
|
||||||
return role, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RebacService) HasRole(userID, resourceID, requiredRole string) (bool, error) {
|
|
||||||
role, err := s.ResolveEffectiveRole(userID, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return role == requiredRole, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RebacService) canGrant(granterRole string) bool {
|
|
||||||
return granterRole == "owner" || granterRole == "admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RebacService) GrantRole(granterID, resourceID, subjectID, role string) error {
|
|
||||||
granterUUID, _ := uuid.Parse(granterID)
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
subjectUUID, _ := uuid.Parse(subjectID)
|
|
||||||
|
|
||||||
granterRole, err := s.ResolveEffectiveRole(granterID, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolve granter role: %w", err)
|
|
||||||
}
|
|
||||||
if !s.canGrant(granterRole) {
|
|
||||||
return fmt.Errorf("granter does not have permission to grant roles")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.queries.CreateRebacRelation(context.Background(), db.CreateRebacRelationParams{
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
SubjectUserID: subjectUUID,
|
|
||||||
Role: role,
|
|
||||||
GrantedBy: granterUUID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create rebac relation: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RebacService) RevokeRole(granterID, resourceID, subjectID string) error {
|
|
||||||
granterRole, err := s.ResolveEffectiveRole(granterID, resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolve granter role: %w", err)
|
|
||||||
}
|
|
||||||
if !s.canGrant(granterRole) {
|
|
||||||
return fmt.Errorf("granter does not have permission to revoke roles")
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
subjectUUID, _ := uuid.Parse(subjectID)
|
|
||||||
return s.queries.DeleteRebacRelation(context.Background(), db.DeleteRebacRelationParams{
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
SubjectUserID: subjectUUID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RebacService) ListShares(resourceID string) ([]db.RebacRelation, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
return s.queries.ListRebacRelationsByResource(context.Background(), resourceUUID)
|
|
||||||
}
|
|
||||||
@@ -1,539 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
"github.com/vaultdrop/backend/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrForbidden = errors.New("forbidden")
|
|
||||||
|
|
||||||
type ResourceService struct {
|
|
||||||
db *sql.DB
|
|
||||||
queries *db.Queries
|
|
||||||
cfg *config.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResourceService(database *sql.DB, queries *db.Queries, cfg *config.Config) *ResourceService {
|
|
||||||
return &ResourceService{db: database, queries: queries, cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) Upload(file *multipart.FileHeader, ownerID string) (*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)
|
|
||||||
}
|
|
||||||
|
|
||||||
h := sha256.New()
|
|
||||||
checksum, err := saveUploadedFile(file, dst, h)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("save file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ownerUUID, err := uuid.Parse(ownerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse owner id: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
existing, err := s.queries.FindDuplicateByChecksum(ctx, db.FindDuplicateByChecksumParams{
|
|
||||||
Checksum: checksum,
|
|
||||||
OwnerID: ownerUUID,
|
|
||||||
})
|
|
||||||
if err == nil && existing.ID != uuid.Nil {
|
|
||||||
os.Remove(dst)
|
|
||||||
placement, err := s.queries.GetServerPlacementByResource(ctx, existing.ID)
|
|
||||||
if err == nil {
|
|
||||||
return &model.UploadResult{
|
|
||||||
ID: existing.ID.String(),
|
|
||||||
Name: existing.Name,
|
|
||||||
Path: placement.StorageKey.String,
|
|
||||||
MimeType: existing.MimeType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("duplicate resource %s has no server placement — upload cannot proceed until resolved", existing.ID.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("begin tx: %w", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
qtx := s.queries.WithTx(tx)
|
|
||||||
|
|
||||||
dbResource, err := qtx.CreateResource(ctx, db.CreateResourceParams{
|
|
||||||
Name: file.Filename,
|
|
||||||
MimeType: file.Header.Get("Content-Type"),
|
|
||||||
Size: file.Size,
|
|
||||||
Checksum: checksum,
|
|
||||||
OwnerID: ownerUUID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create resource in db: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
placement, err := s.ensureServerPlacementQtx(qtx, dbResource.ID, ownerUUID, dst)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create server placement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := qtx.CreateRebacRelation(ctx, db.CreateRebacRelationParams{
|
|
||||||
ResourceID: dbResource.ID,
|
|
||||||
SubjectUserID: ownerUUID,
|
|
||||||
Role: "owner",
|
|
||||||
GrantedBy: ownerUUID,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, fmt.Errorf("create owner rebac: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return nil, fmt.Errorf("commit tx: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.UploadResult{
|
|
||||||
ID: dbResource.ID.String(),
|
|
||||||
Name: dbResource.Name,
|
|
||||||
Path: placement.StorageKey.String,
|
|
||||||
MimeType: dbResource.MimeType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) ensureServerPlacement(resourceID, ownerID uuid.UUID, dst string) (db.ResourcePlacement, error) {
|
|
||||||
return s.ensureServerPlacementQtx(s.queries, resourceID, ownerID, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) ensureServerPlacementQtx(q *db.Queries, resourceID, ownerID uuid.UUID, dst string) (db.ResourcePlacement, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
serverLoc, err := q.GetServerStorageLocation(ctx, ownerID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
serverLoc, err = q.CreateStorageLocation(ctx, db.CreateStorageLocationParams{
|
|
||||||
UserID: ownerID,
|
|
||||||
DeviceName: "VaultDrop Server",
|
|
||||||
Role: "server",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return db.ResourcePlacement{}, fmt.Errorf("get/create server location: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
placement, err := q.CreatePlacement(ctx, db.CreatePlacementParams{
|
|
||||||
ResourceID: resourceID,
|
|
||||||
StorageLocationID: serverLoc.ID,
|
|
||||||
Status: "synced",
|
|
||||||
StorageKey: sql.NullString{String: dst, Valid: true},
|
|
||||||
SyncedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return db.ResourcePlacement{}, fmt.Errorf("create placement: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return placement, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) List(ownerID string, page, limit int) ([]model.Resource, int, error) {
|
|
||||||
ownerUUID, _ := uuid.Parse(ownerID)
|
|
||||||
|
|
||||||
total, err := s.queries.CountResourcesByOwner(context.Background(), ownerUUID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("count resources: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
dbResources, err := s.queries.ListResourcesByOwner(context.Background(), db.ListResourcesByOwnerParams{
|
|
||||||
OwnerID: ownerUUID,
|
|
||||||
Limit: int32(limit),
|
|
||||||
Offset: int32(offset),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("list resources: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resources := make([]model.Resource, len(dbResources))
|
|
||||||
for i, r := range dbResources {
|
|
||||||
tags, err := s.queries.GetTagsByResourceID(context.Background(), r.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("get tags for resource %s: %w", r.ID, err)
|
|
||||||
}
|
|
||||||
resources[i] = dbResourceToModel(r, tags)
|
|
||||||
}
|
|
||||||
return resources, int(total), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) Get(id string) (*model.Resource, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(id)
|
|
||||||
r, err := s.queries.GetResource(context.Background(), resourceUUID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get resource: %w", err)
|
|
||||||
}
|
|
||||||
tags, err := s.queries.GetTagsByResourceID(context.Background(), r.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get tags: %w", err)
|
|
||||||
}
|
|
||||||
m := dbResourceToModel(r, tags)
|
|
||||||
return &m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type DeleteResult struct {
|
|
||||||
StoragePaths []string
|
|
||||||
Variants []model.Variant
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) Delete(id string) error {
|
|
||||||
resourceUUID, _ := uuid.Parse(id)
|
|
||||||
return s.queries.DeleteResource(context.Background(), resourceUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) DeleteRecursive(id, userID string) (*DeleteResult, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(id)
|
|
||||||
ownerUUID, err := uuid.Parse(userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, ErrForbidden
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := s.queries.GetResource(context.Background(), resourceUUID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get resource: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.OwnerID != ownerUUID {
|
|
||||||
return nil, ErrForbidden
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &DeleteResult{}
|
|
||||||
|
|
||||||
if r.IsFolder {
|
|
||||||
children, err := s.queries.ListResourcesByParentID(context.Background(), uuid.NullUUID{UUID: resourceUUID, Valid: true})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list children: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, child := range children {
|
|
||||||
childResult, err := s.DeleteRecursive(child.ID.String(), userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("delete child %s: %w", child.ID, err)
|
|
||||||
}
|
|
||||||
result.StoragePaths = append(result.StoragePaths, childResult.StoragePaths...)
|
|
||||||
result.Variants = append(result.Variants, childResult.Variants...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.queries.DeleteResource(context.Background(), resourceUUID); err != nil {
|
|
||||||
return nil, fmt.Errorf("delete resource: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
storagePath, _ := s.GetStoragePath(id)
|
|
||||||
if storagePath != "" {
|
|
||||||
result.StoragePaths = append(result.StoragePaths, storagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
variants, _ := s.GetVariantsByResourceID(id)
|
|
||||||
result.Variants = append(result.Variants, variants...)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) GetStoragePath(id string) (string, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(id)
|
|
||||||
placement, err := s.queries.GetServerPlacementByResource(context.Background(), resourceUUID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get server placement for resource %s: %w", id, err)
|
|
||||||
}
|
|
||||||
return placement.StorageKey.String, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) UpdateOCRText(id, text string) error {
|
|
||||||
resourceUUID, _ := uuid.Parse(id)
|
|
||||||
r, err := s.queries.GetResource(context.Background(), resourceUUID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get resource: %w", err)
|
|
||||||
}
|
|
||||||
return s.queries.UpdateResource(context.Background(), db.UpdateResourceParams{
|
|
||||||
Name: r.Name,
|
|
||||||
MimeType: r.MimeType,
|
|
||||||
OcrText: text,
|
|
||||||
ID: resourceUUID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) AddTags(resourceID string, tagNames []string) error {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
for _, name := range tagNames {
|
|
||||||
tag, err := s.queries.GetTagByName(context.Background(), name)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
tag, err = s.queries.CreateTag(context.Background(), name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create tag %q: %w", name, err)
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("get tag %q: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.queries.AddTagToResource(context.Background(), db.AddTagToResourceParams{
|
|
||||||
TagID: tag.ID,
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("link tag %q to resource: %w", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) GetTagsByResourceID(resourceID string) ([]model.Tag, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
dbTags, err := s.queries.GetTagsByResourceID(context.Background(), resourceUUID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get tags: %w", err)
|
|
||||||
}
|
|
||||||
tags := make([]model.Tag, len(dbTags))
|
|
||||||
for i, t := range dbTags {
|
|
||||||
tags[i] = model.Tag{ID: t.ID.String(), Name: t.TagName}
|
|
||||||
}
|
|
||||||
return tags, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) MoveResources(resourceIDs []string, parentResourceID *string) error {
|
|
||||||
uuids := make([]uuid.UUID, len(resourceIDs))
|
|
||||||
for i, id := range resourceIDs {
|
|
||||||
uuids[i], _ = uuid.Parse(id)
|
|
||||||
}
|
|
||||||
var parentID uuid.NullUUID
|
|
||||||
if parentResourceID != nil {
|
|
||||||
pid, _ := uuid.Parse(*parentResourceID)
|
|
||||||
parentID = uuid.NullUUID{UUID: pid, Valid: true}
|
|
||||||
}
|
|
||||||
return s.queries.MoveResources(context.Background(), db.MoveResourcesParams{
|
|
||||||
ParentResourceID: parentID,
|
|
||||||
Column2: uuids,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) CreateFolder(name, ownerID string, parentResourceID *string) (*model.Resource, error) {
|
|
||||||
ownerUUID, _ := uuid.Parse(ownerID)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("begin tx: %w", err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
qtx := s.queries.WithTx(tx)
|
|
||||||
|
|
||||||
parentID := uuid.NullUUID{Valid: false}
|
|
||||||
if parentResourceID != nil {
|
|
||||||
pid, _ := uuid.Parse(*parentResourceID)
|
|
||||||
parentID = uuid.NullUUID{UUID: pid, Valid: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := qtx.CreateFolder(ctx, db.CreateFolderParams{
|
|
||||||
Name: name,
|
|
||||||
OwnerID: ownerUUID,
|
|
||||||
ParentResourceID: parentID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create folder: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := qtx.CreateRebacRelation(ctx, db.CreateRebacRelationParams{
|
|
||||||
ResourceID: r.ID,
|
|
||||||
SubjectUserID: ownerUUID,
|
|
||||||
Role: "owner",
|
|
||||||
GrantedBy: ownerUUID,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, fmt.Errorf("create owner rebac: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return nil, fmt.Errorf("commit tx: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := dbResourceToModel(r, nil)
|
|
||||||
return &m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) ListFolders(ownerID string) ([]model.Resource, error) {
|
|
||||||
dbResources, err := s.queries.ListFolders(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list folders: %w", err)
|
|
||||||
}
|
|
||||||
folders := make([]model.Resource, len(dbResources))
|
|
||||||
for i, r := range dbResources {
|
|
||||||
folders[i] = dbResourceToModel(r, nil)
|
|
||||||
}
|
|
||||||
return folders, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) ListResourcesByParentID(parentID, ownerID string, page, limit int) ([]model.Resource, int, error) {
|
|
||||||
parentUUID, _ := uuid.Parse(parentID)
|
|
||||||
ownerUUID, _ := uuid.Parse(ownerID)
|
|
||||||
|
|
||||||
total, err := s.queries.CountResourcesByParentAndOwner(context.Background(), db.CountResourcesByParentAndOwnerParams{
|
|
||||||
ParentResourceID: uuid.NullUUID{UUID: parentUUID, Valid: true},
|
|
||||||
OwnerID: ownerUUID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("count resources by parent: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
dbResources, err := s.queries.ListResourcesByParentAndOwner(context.Background(), db.ListResourcesByParentAndOwnerParams{
|
|
||||||
ParentResourceID: uuid.NullUUID{UUID: parentUUID, Valid: true},
|
|
||||||
OwnerID: ownerUUID,
|
|
||||||
Limit: int32(limit),
|
|
||||||
Offset: int32(offset),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("list resources by parent: %w", err)
|
|
||||||
}
|
|
||||||
resources := make([]model.Resource, len(dbResources))
|
|
||||||
for i, r := range dbResources {
|
|
||||||
tags, err := s.queries.GetTagsByResourceID(context.Background(), r.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("get tags for resource %s: %w", r.ID, err)
|
|
||||||
}
|
|
||||||
resources[i] = dbResourceToModel(r, tags)
|
|
||||||
}
|
|
||||||
return resources, int(total), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) GetVariantsByResourceID(resourceID string) ([]model.Variant, error) {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
dbVariants, err := s.queries.GetVariantsByResourceID(context.Background(), resourceUUID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get variants: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
variants := make([]model.Variant, len(dbVariants))
|
|
||||||
for i, v := range dbVariants {
|
|
||||||
variants[i] = model.Variant{
|
|
||||||
ID: v.ID.String(),
|
|
||||||
ResourceID: v.ResourceID.String(),
|
|
||||||
VariantType: v.VariantType,
|
|
||||||
PageNumber: int(v.PageNumber),
|
|
||||||
Width: int(v.Width),
|
|
||||||
Height: int(v.Height),
|
|
||||||
StorageKey: v.StorageKey,
|
|
||||||
MimeType: v.MimeType,
|
|
||||||
GeneratedBy: v.GeneratedBy,
|
|
||||||
CreatedAt: v.CreatedAt.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return variants, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) GetVariantStoragePath(id string) (string, error) {
|
|
||||||
variantUUID, _ := uuid.Parse(id)
|
|
||||||
v, err := s.queries.GetVariantByID(context.Background(), variantUUID)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get variant: %w", err)
|
|
||||||
}
|
|
||||||
return v.StorageKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) GetBestVariant(resourceID, preferredType string) *model.Variant {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
dbVariants, err := s.queries.GetVariantsByResourceID(context.Background(), resourceUUID)
|
|
||||||
if err != nil || len(dbVariants) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallback *model.Variant
|
|
||||||
for _, v := range dbVariants {
|
|
||||||
if v.PageNumber != 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mv := &model.Variant{
|
|
||||||
ID: v.ID.String(),
|
|
||||||
ResourceID: v.ResourceID.String(),
|
|
||||||
VariantType: v.VariantType,
|
|
||||||
PageNumber: int(v.PageNumber),
|
|
||||||
Width: int(v.Width),
|
|
||||||
Height: int(v.Height),
|
|
||||||
StorageKey: v.StorageKey,
|
|
||||||
MimeType: v.MimeType,
|
|
||||||
GeneratedBy: v.GeneratedBy,
|
|
||||||
CreatedAt: v.CreatedAt.String(),
|
|
||||||
}
|
|
||||||
if v.VariantType == preferredType {
|
|
||||||
return mv
|
|
||||||
}
|
|
||||||
if fallback == nil {
|
|
||||||
fallback = mv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ResourceService) FindDuplicatesByNameSize(name string, size int64) ([]db.FindDuplicatesByNameSizeRow, error) {
|
|
||||||
return s.queries.FindDuplicatesByNameSize(context.Background(), db.FindDuplicatesByNameSizeParams{
|
|
||||||
Name: name,
|
|
||||||
Size: size,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbResourceToModel(r db.Resource, dbTags []db.Tag) model.Resource {
|
|
||||||
tags := make([]model.Tag, len(dbTags))
|
|
||||||
for i, t := range dbTags {
|
|
||||||
tags[i] = model.Tag{ID: t.ID.String(), Name: t.TagName}
|
|
||||||
}
|
|
||||||
|
|
||||||
parentID := ""
|
|
||||||
if r.ParentResourceID.Valid {
|
|
||||||
parentID = r.ParentResourceID.UUID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return model.Resource{
|
|
||||||
ID: r.ID.String(),
|
|
||||||
Name: r.Name,
|
|
||||||
MimeType: r.MimeType,
|
|
||||||
Size: r.Size,
|
|
||||||
OcrText: r.OcrText,
|
|
||||||
IsFolder: r.IsFolder,
|
|
||||||
ParentResourceID: parentID,
|
|
||||||
OwnerID: r.OwnerID.String(),
|
|
||||||
Tags: tags,
|
|
||||||
CreatedAt: r.CreatedAt,
|
|
||||||
UpdatedAt: r.UpdatedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveUploadedFile(file *multipart.FileHeader, dst string, h hash.Hash) (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()
|
|
||||||
|
|
||||||
writer := io.MultiWriter(out, h)
|
|
||||||
if _, err := io.CopyN(writer, src, file.Size); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SyncService struct {
|
|
||||||
queries *db.Queries
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSyncService(queries *db.Queries) *SyncService {
|
|
||||||
return &SyncService{queries: queries}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncService) EnqueueUpload(resourceID, locationID string) error {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
locationUUID, _ := uuid.Parse(locationID)
|
|
||||||
_, err := s.queries.CreateSyncQueueItem(context.Background(), db.CreateSyncQueueItemParams{
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
StorageLocationID: locationUUID,
|
|
||||||
Operation: "upload",
|
|
||||||
Status: "pending",
|
|
||||||
Attempts: 0,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncService) EnqueueDownload(resourceID, locationID string) error {
|
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
|
||||||
locationUUID, _ := uuid.Parse(locationID)
|
|
||||||
_, err := s.queries.CreateSyncQueueItem(context.Background(), db.CreateSyncQueueItemParams{
|
|
||||||
ResourceID: resourceUUID,
|
|
||||||
StorageLocationID: locationUUID,
|
|
||||||
Operation: "download",
|
|
||||||
Status: "pending",
|
|
||||||
Attempts: 0,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncService) ListPending(locationID string) ([]db.SyncQueue, error) {
|
|
||||||
locationUUID, _ := uuid.Parse(locationID)
|
|
||||||
return s.queries.ListPendingSyncItemsByLocation(context.Background(), locationUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncService) ListAllPending() ([]db.SyncQueue, error) {
|
|
||||||
return s.queries.ListPendingSyncItems(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncService) MarkCompleted(queueID string) error {
|
|
||||||
id, _ := uuid.Parse(queueID)
|
|
||||||
return s.queries.UpdateSyncQueueStatus(context.Background(), db.UpdateSyncQueueStatusParams{
|
|
||||||
Status: "completed",
|
|
||||||
Attempts: 0,
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SyncService) MarkFailed(queueID string, errMsg string) error {
|
|
||||||
id, _ := uuid.Parse(queueID)
|
|
||||||
item, err := s.queries.GetSyncQueueItem(context.Background(), id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get sync queue item: %w", err)
|
|
||||||
}
|
|
||||||
return s.queries.UpdateSyncQueueStatus(context.Background(), db.UpdateSyncQueueStatusParams{
|
|
||||||
Status: "failed",
|
|
||||||
Attempts: int32(item.Attempts + 1),
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type URLService struct {
|
|
||||||
secret string
|
|
||||||
serverHost string
|
|
||||||
expiryDuration time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewURLService(secret, serverHost string, expiryMinutes int) *URLService {
|
|
||||||
if expiryMinutes <= 0 {
|
|
||||||
expiryMinutes = 60
|
|
||||||
}
|
|
||||||
return &URLService{
|
|
||||||
secret: secret,
|
|
||||||
serverHost: serverHost,
|
|
||||||
expiryDuration: time.Duration(expiryMinutes) * time.Minute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *URLService) sign(id string, expires int64) string {
|
|
||||||
data := fmt.Sprintf("%s:%d", id, expires)
|
|
||||||
mac := hmac.New(sha256.New, []byte(s.secret))
|
|
||||||
mac.Write([]byte(data))
|
|
||||||
return hex.EncodeToString(mac.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *URLService) GenerateDownloadURL(resourceUUID string) string {
|
|
||||||
expires := time.Now().Add(s.expiryDuration).Unix()
|
|
||||||
sig := s.sign(resourceUUID, expires)
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s/api/v1/resources/download/%s?expires=%d&sig=%s",
|
|
||||||
s.serverHost,
|
|
||||||
resourceUUID,
|
|
||||||
expires,
|
|
||||||
sig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *URLService) GenerateVariantURL(variantUUID string) string {
|
|
||||||
expires := time.Now().Add(s.expiryDuration).Unix()
|
|
||||||
sig := s.sign(variantUUID, expires)
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s/api/v1/variants/%s?expires=%d&sig=%s",
|
|
||||||
s.serverHost,
|
|
||||||
variantUUID,
|
|
||||||
expires,
|
|
||||||
sig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *URLService) Validate(id, sig string, expires int64) bool {
|
|
||||||
if time.Now().Unix() > expires {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
expected := s.sign(id, expires)
|
|
||||||
return hmac.Equal([]byte(sig), []byte(expected))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserCreation(t *testing.T) {
|
||||||
|
|
||||||
|
err, userAntoine := NewUser("antoine")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(`Error creating antoine user %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err, userBob := NewUser("bob")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(`Error creating bob user %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(userAntoine, userBob)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateDocument(t *testing.T) {
|
||||||
|
|
||||||
|
err, document := NewDocument("paper.pdf", FILE)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(`Error creating document %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err, directory := NewDocument("bob", DIRECTORY)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf(`Error creating bob directory %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(document, directory)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
FROM python:3.10-slim
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libglib2.0-0 libgl1 libgomp1 curl \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN pip install --no-cache-dir \
|
|
||||||
paddlepaddle==3.0.0 \
|
|
||||||
paddleocr==3.3.3 \
|
|
||||||
paddlex==3.3.13 \
|
|
||||||
fastapi \
|
|
||||||
uvicorn \
|
|
||||||
python-multipart \
|
|
||||||
Pillow \
|
|
||||||
numpy \
|
|
||||||
pypdfium2
|
|
||||||
|
|
||||||
COPY server.py /workspace/server.py
|
|
||||||
|
|
||||||
WORKDIR /workspace
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import os
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pypdfium2 as pdfium
|
|
||||||
from fastapi import FastAPI, UploadFile, File
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from paddleocr import PaddleOCR
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger("paddleocr-server")
|
|
||||||
|
|
||||||
OCR_LANG = os.getenv("OCR_LANG", "fr")
|
|
||||||
|
|
||||||
print(f"[INIT] Initializing PaddleOCR (lang={OCR_LANG})...", flush=True)
|
|
||||||
ocr_engine = PaddleOCR(
|
|
||||||
use_doc_orientation_classify=False,
|
|
||||||
use_doc_unwarping=False,
|
|
||||||
use_textline_orientation=False,
|
|
||||||
lang=OCR_LANG,
|
|
||||||
)
|
|
||||||
print("[INIT] PaddleOCR ready.", flush=True)
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
|
|
||||||
class OCRRequest(BaseModel):
|
|
||||||
image: str
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
def health():
|
|
||||||
return {"status": "healthy", "service": "PaddleOCR Server"}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/ocr")
|
|
||||||
def ocr_json(req: OCRRequest):
|
|
||||||
print(f"[OCR] Request received, image field length: {len(req.image)}", flush=True)
|
|
||||||
try:
|
|
||||||
img_bytes = base64.b64decode(req.image)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[OCR] Base64 decode failed: {e}", flush=True)
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=400,
|
|
||||||
content={"errorCode": 1, "message": "invalid base64 image"},
|
|
||||||
)
|
|
||||||
print(f"[OCR] Decoded {len(img_bytes)} bytes, header: {img_bytes[:32].hex()}", flush=True)
|
|
||||||
return run_ocr(img_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/ocr/upload")
|
|
||||||
def ocr_upload(file: UploadFile = File(...)):
|
|
||||||
img_bytes = file.file.read()
|
|
||||||
print(f"[OCR] Upload received, {len(img_bytes)} bytes, header: {img_bytes[:32].hex()}", flush=True)
|
|
||||||
return run_ocr(img_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_pdf(data: bytes) -> bool:
|
|
||||||
return data[:5] == b"%PDF-"
|
|
||||||
|
|
||||||
|
|
||||||
def _pdf_to_images(pdf_bytes: bytes):
|
|
||||||
doc = pdfium.PdfDocument(BytesIO(pdf_bytes))
|
|
||||||
images = []
|
|
||||||
for i in range(len(doc)):
|
|
||||||
page = doc[i]
|
|
||||||
bitmap = page.render(scale=3)
|
|
||||||
pil_image = bitmap.to_pil()
|
|
||||||
images.append(pil_image.convert("RGB"))
|
|
||||||
doc.close()
|
|
||||||
return images
|
|
||||||
|
|
||||||
|
|
||||||
def run_ocr(img_bytes: bytes):
|
|
||||||
try:
|
|
||||||
if _is_pdf(img_bytes):
|
|
||||||
images = _pdf_to_images(img_bytes)
|
|
||||||
print(f"[OCR] PDF: {len(images)} pages", flush=True)
|
|
||||||
else:
|
|
||||||
images = [Image.open(BytesIO(img_bytes)).convert("RGB")]
|
|
||||||
|
|
||||||
pages = []
|
|
||||||
for i, image in enumerate(images):
|
|
||||||
img_array = np.array(image)
|
|
||||||
print(f"[OCR] Page {i+1}/{len(images)}: {img_array.shape}", flush=True)
|
|
||||||
result = list(ocr_engine.predict(img_array))
|
|
||||||
|
|
||||||
texts, scores, boxes, polys = [], [], [], []
|
|
||||||
for r in result:
|
|
||||||
raw = r._to_json()
|
|
||||||
data = raw.get("res", raw)
|
|
||||||
texts.extend(data.get("rec_texts", []))
|
|
||||||
scores.extend(float(s) for s in data.get("rec_scores", []))
|
|
||||||
boxes.extend(data.get("rec_boxes", []))
|
|
||||||
polys.extend(data.get("rec_polys", []))
|
|
||||||
|
|
||||||
pages.append({
|
|
||||||
"rec_texts": texts,
|
|
||||||
"rec_scores": scores,
|
|
||||||
"rec_boxes": boxes,
|
|
||||||
"rec_polys": polys,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"errorCode": 0, "result": {"ocrResults": pages}}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("OCR failed")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=500,
|
|
||||||
content={"errorCode": 2, "message": str(e)},
|
|
||||||
)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Success(c *gin.Context, data interface{}) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"data": data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Created(c *gin.Context, data interface{}) {
|
|
||||||
c.JSON(http.StatusCreated, gin.H{
|
|
||||||
"data": data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paginated(c *gin.Context, data interface{}, page, total int) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"data": data,
|
|
||||||
"meta": gin.H{
|
|
||||||
"page": page,
|
|
||||||
"total": total,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Error(c *gin.Context, status int, code, message string) {
|
|
||||||
c.JSON(status, gin.H{
|
|
||||||
"error": gin.H{
|
|
||||||
"code": code,
|
|
||||||
"message": message,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
version: "2"
|
|
||||||
sql:
|
|
||||||
- engine: "postgresql"
|
|
||||||
queries: "internal/db/queries/"
|
|
||||||
schema: "internal/db/migrations"
|
|
||||||
gen:
|
|
||||||
go:
|
|
||||||
package: "db"
|
|
||||||
out: "internal/db"
|
|
||||||
sql_package: "database/sql"
|
|
||||||
emit_json_tags: true
|
|
||||||
emit_db_tags: false
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(username string) (error, User) {
|
||||||
|
return nil, User{
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user