diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 030ff36..0000000 --- a/backend/.gitignore +++ /dev/null @@ -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 diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 9695387..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -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"] diff --git a/backend/cmd/gc/main.go b/backend/cmd/gc/main.go deleted file mode 100644 index 3482499..0000000 --- a/backend/cmd/gc/main.go +++ /dev/null @@ -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)) -} diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go deleted file mode 100644 index 96d05da..0000000 --- a/backend/cmd/server/main.go +++ /dev/null @@ -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) - } -} diff --git a/backend/documents.go b/backend/documents.go new file mode 100644 index 0000000..9119aa2 --- /dev/null +++ b/backend/documents.go @@ -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, + } + +} diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go deleted file mode 100644 index 35c0478..0000000 --- a/backend/internal/auth/handler.go +++ /dev/null @@ -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"}) -} diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go deleted file mode 100644 index e19911f..0000000 --- a/backend/internal/auth/middleware.go +++ /dev/null @@ -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() - } -} diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go deleted file mode 100644 index c3d9bfa..0000000 --- a/backend/internal/auth/service.go +++ /dev/null @@ -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[:]) -} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go deleted file mode 100644 index 01fd802..0000000 --- a/backend/internal/config/config.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/auth.sql.go b/backend/internal/db/auth.sql.go deleted file mode 100644 index 2fb135d..0000000 --- a/backend/internal/db/auth.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/connect.go b/backend/internal/db/connect.go deleted file mode 100644 index ceb682c..0000000 --- a/backend/internal/db/connect.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go deleted file mode 100644 index f43598b..0000000 --- a/backend/internal/db/db.go +++ /dev/null @@ -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, - } -} diff --git a/backend/internal/db/health.sql.go b/backend/internal/db/health.sql.go deleted file mode 100644 index 8053e1b..0000000 --- a/backend/internal/db/health.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/migrate.go b/backend/internal/db/migrate.go deleted file mode 100644 index 44e1429..0000000 --- a/backend/internal/db/migrate.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/migrations/001_init.down.sql b/backend/internal/db/migrations/001_init.down.sql deleted file mode 100644 index fce3daf..0000000 --- a/backend/internal/db/migrations/001_init.down.sql +++ /dev/null @@ -1 +0,0 @@ --- Rollback 001_init diff --git a/backend/internal/db/migrations/001_init.up.sql b/backend/internal/db/migrations/001_init.up.sql deleted file mode 100644 index c709c31..0000000 --- a/backend/internal/db/migrations/001_init.up.sql +++ /dev/null @@ -1,3 +0,0 @@ --- VaultDrop 001: Initial schema --- This is a placeholder. Add your first CREATE TABLE here. -SELECT 1; diff --git a/backend/internal/db/migrations/002_files.down.sql b/backend/internal/db/migrations/002_files.down.sql deleted file mode 100644 index 1e0a59b..0000000 --- a/backend/internal/db/migrations/002_files.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS files; diff --git a/backend/internal/db/migrations/002_files.up.sql b/backend/internal/db/migrations/002_files.up.sql deleted file mode 100644 index a802bf8..0000000 --- a/backend/internal/db/migrations/002_files.up.sql +++ /dev/null @@ -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 -); diff --git a/backend/internal/db/migrations/003_tags.down.sql b/backend/internal/db/migrations/003_tags.down.sql deleted file mode 100644 index e5f21c4..0000000 --- a/backend/internal/db/migrations/003_tags.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP TABLE IF EXISTS file_tags; - -DROP TABLE IF EXISTS tags; \ No newline at end of file diff --git a/backend/internal/db/migrations/003_tags.up.sql b/backend/internal/db/migrations/003_tags.up.sql deleted file mode 100644 index 4a404bb..0000000 --- a/backend/internal/db/migrations/003_tags.up.sql +++ /dev/null @@ -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) -) diff --git a/backend/internal/db/migrations/004_folders.down.sql b/backend/internal/db/migrations/004_folders.down.sql deleted file mode 100644 index 6de30ce..0000000 --- a/backend/internal/db/migrations/004_folders.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- VaultDrop 003: tags table -ALTER TABLE files DROP COLUMN IF EXISTS is_folder; \ No newline at end of file diff --git a/backend/internal/db/migrations/004_folders.up.sql b/backend/internal/db/migrations/004_folders.up.sql deleted file mode 100644 index 6628717..0000000 --- a/backend/internal/db/migrations/004_folders.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false; \ No newline at end of file diff --git a/backend/internal/db/migrations/005_folders.down.sql b/backend/internal/db/migrations/005_folders.down.sql deleted file mode 100644 index a36242d..0000000 --- a/backend/internal/db/migrations/005_folders.down.sql +++ /dev/null @@ -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; \ No newline at end of file diff --git a/backend/internal/db/migrations/005_folders.up.sql b/backend/internal/db/migrations/005_folders.up.sql deleted file mode 100644 index bba7447..0000000 --- a/backend/internal/db/migrations/005_folders.up.sql +++ /dev/null @@ -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) \ No newline at end of file diff --git a/backend/internal/db/migrations/006_folders.down.sql b/backend/internal/db/migrations/006_folders.down.sql deleted file mode 100644 index 6628717..0000000 --- a/backend/internal/db/migrations/006_folders.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false; \ No newline at end of file diff --git a/backend/internal/db/migrations/006_folders.up.sql b/backend/internal/db/migrations/006_folders.up.sql deleted file mode 100644 index 6de30ce..0000000 --- a/backend/internal/db/migrations/006_folders.up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- VaultDrop 003: tags table -ALTER TABLE files DROP COLUMN IF EXISTS is_folder; \ No newline at end of file diff --git a/backend/internal/db/migrations/007_add_is_folder.down.sql b/backend/internal/db/migrations/007_add_is_folder.down.sql deleted file mode 100644 index c8c7736..0000000 --- a/backend/internal/db/migrations/007_add_is_folder.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE files DROP COLUMN IF EXISTS is_folder; diff --git a/backend/internal/db/migrations/007_add_is_folder.up.sql b/backend/internal/db/migrations/007_add_is_folder.up.sql deleted file mode 100644 index 421f503..0000000 --- a/backend/internal/db/migrations/007_add_is_folder.up.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/migrations/008_uuid_default.down.sql b/backend/internal/db/migrations/008_uuid_default.down.sql deleted file mode 100644 index 791e8a2..0000000 --- a/backend/internal/db/migrations/008_uuid_default.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE files ALTER COLUMN id DROP DEFAULT; diff --git a/backend/internal/db/migrations/008_uuid_default.up.sql b/backend/internal/db/migrations/008_uuid_default.up.sql deleted file mode 100644 index 65b7328..0000000 --- a/backend/internal/db/migrations/008_uuid_default.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE files ALTER COLUMN id SET DEFAULT gen_random_uuid(); -ALTER TABLE files ALTER COLUMN storage_key SET DEFAULT ''; diff --git a/backend/internal/db/migrations/009_users_default.down.sql b/backend/internal/db/migrations/009_users_default.down.sql deleted file mode 100644 index 555e0ea..0000000 --- a/backend/internal/db/migrations/009_users_default.down.sql +++ /dev/null @@ -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 -); diff --git a/backend/internal/db/migrations/009_users_default.up.sql b/backend/internal/db/migrations/009_users_default.up.sql deleted file mode 100644 index 365a210..0000000 --- a/backend/internal/db/migrations/009_users_default.up.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/backend/internal/db/migrations/010_auth.down.sql b/backend/internal/db/migrations/010_auth.down.sql deleted file mode 100644 index 7150d35..0000000 --- a/backend/internal/db/migrations/010_auth.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE IF EXISTS refresh_tokens; -DROP TABLE IF EXISTS users; diff --git a/backend/internal/db/migrations/010_auth.up.sql b/backend/internal/db/migrations/010_auth.up.sql deleted file mode 100644 index 9b3ea73..0000000 --- a/backend/internal/db/migrations/010_auth.up.sql +++ /dev/null @@ -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); diff --git a/backend/internal/db/migrations/011_thumbnails.down.sql b/backend/internal/db/migrations/011_thumbnails.down.sql deleted file mode 100644 index 06a3ee4..0000000 --- a/backend/internal/db/migrations/011_thumbnails.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS thumbnails; diff --git a/backend/internal/db/migrations/011_thumbnails.up.sql b/backend/internal/db/migrations/011_thumbnails.up.sql deleted file mode 100644 index b47ab38..0000000 --- a/backend/internal/db/migrations/011_thumbnails.up.sql +++ /dev/null @@ -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); diff --git a/backend/internal/db/migrations/012_v3_schema.down.sql b/backend/internal/db/migrations/012_v3_schema.down.sql deleted file mode 100644 index e25753b..0000000 --- a/backend/internal/db/migrations/012_v3_schema.down.sql +++ /dev/null @@ -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 -); diff --git a/backend/internal/db/migrations/012_v3_schema.up.sql b/backend/internal/db/migrations/012_v3_schema.up.sql deleted file mode 100644 index 7868c8d..0000000 --- a/backend/internal/db/migrations/012_v3_schema.up.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/migrations/013_unique_server_location.down.sql b/backend/internal/db/migrations/013_unique_server_location.down.sql deleted file mode 100644 index 2070edb..0000000 --- a/backend/internal/db/migrations/013_unique_server_location.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS idx_storage_locations_user_server; diff --git a/backend/internal/db/migrations/013_unique_server_location.up.sql b/backend/internal/db/migrations/013_unique_server_location.up.sql deleted file mode 100644 index a0b9971..0000000 --- a/backend/internal/db/migrations/013_unique_server_location.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE UNIQUE INDEX IF NOT EXISTS idx_storage_locations_user_server -ON storage_locations(user_id) WHERE role = 'server'; diff --git a/backend/internal/db/migrations/014_ocr_jobs.down.sql b/backend/internal/db/migrations/014_ocr_jobs.down.sql deleted file mode 100644 index a1f7ec1..0000000 --- a/backend/internal/db/migrations/014_ocr_jobs.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS ocr_jobs; diff --git a/backend/internal/db/migrations/014_ocr_jobs.up.sql b/backend/internal/db/migrations/014_ocr_jobs.up.sql deleted file mode 100644 index f960b7b..0000000 --- a/backend/internal/db/migrations/014_ocr_jobs.up.sql +++ /dev/null @@ -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); diff --git a/backend/internal/db/migrations/015_resource_tags_cascade.down.sql b/backend/internal/db/migrations/015_resource_tags_cascade.down.sql deleted file mode 100644 index d26b2eb..0000000 --- a/backend/internal/db/migrations/015_resource_tags_cascade.down.sql +++ /dev/null @@ -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); diff --git a/backend/internal/db/migrations/015_resource_tags_cascade.up.sql b/backend/internal/db/migrations/015_resource_tags_cascade.up.sql deleted file mode 100644 index 82c56cd..0000000 --- a/backend/internal/db/migrations/015_resource_tags_cascade.up.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/models.go b/backend/internal/db/models.go deleted file mode 100644 index c5e961e..0000000 --- a/backend/internal/db/models.go +++ /dev/null @@ -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"` -} diff --git a/backend/internal/db/ocr_jobs.sql.go b/backend/internal/db/ocr_jobs.sql.go deleted file mode 100644 index 2086a2b..0000000 --- a/backend/internal/db/ocr_jobs.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/queries/auth.sql b/backend/internal/db/queries/auth.sql deleted file mode 100644 index cc88066..0000000 --- a/backend/internal/db/queries/auth.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/health.sql b/backend/internal/db/queries/health.sql deleted file mode 100644 index c98cd10..0000000 --- a/backend/internal/db/queries/health.sql +++ /dev/null @@ -1,2 +0,0 @@ --- name: GetHealth :one -SELECT 1 AS ok; diff --git a/backend/internal/db/queries/ocr_jobs.sql b/backend/internal/db/queries/ocr_jobs.sql deleted file mode 100644 index 2398f38..0000000 --- a/backend/internal/db/queries/ocr_jobs.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/rebac_relations.sql b/backend/internal/db/queries/rebac_relations.sql deleted file mode 100644 index 6616f70..0000000 --- a/backend/internal/db/queries/rebac_relations.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/resource_placements.sql b/backend/internal/db/queries/resource_placements.sql deleted file mode 100644 index d4e5799..0000000 --- a/backend/internal/db/queries/resource_placements.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/resource_variants.sql b/backend/internal/db/queries/resource_variants.sql deleted file mode 100644 index 2b4d4f6..0000000 --- a/backend/internal/db/queries/resource_variants.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/resources.sql b/backend/internal/db/queries/resources.sql deleted file mode 100644 index e5db7fd..0000000 --- a/backend/internal/db/queries/resources.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/retention_policies.sql b/backend/internal/db/queries/retention_policies.sql deleted file mode 100644 index 2963483..0000000 --- a/backend/internal/db/queries/retention_policies.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/storage_locations.sql b/backend/internal/db/queries/storage_locations.sql deleted file mode 100644 index bbfc378..0000000 --- a/backend/internal/db/queries/storage_locations.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/sync_queue.sql b/backend/internal/db/queries/sync_queue.sql deleted file mode 100644 index 7a22011..0000000 --- a/backend/internal/db/queries/sync_queue.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/queries/tags.sql b/backend/internal/db/queries/tags.sql deleted file mode 100644 index 2d1f2cf..0000000 --- a/backend/internal/db/queries/tags.sql +++ /dev/null @@ -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; diff --git a/backend/internal/db/rebac_relations.sql.go b/backend/internal/db/rebac_relations.sql.go deleted file mode 100644 index 9230cc2..0000000 --- a/backend/internal/db/rebac_relations.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/resource_placements.sql.go b/backend/internal/db/resource_placements.sql.go deleted file mode 100644 index f484207..0000000 --- a/backend/internal/db/resource_placements.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/resource_variants.sql.go b/backend/internal/db/resource_variants.sql.go deleted file mode 100644 index 317ae28..0000000 --- a/backend/internal/db/resource_variants.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/resources.sql.go b/backend/internal/db/resources.sql.go deleted file mode 100644 index 550c4d6..0000000 --- a/backend/internal/db/resources.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/retention_policies.sql.go b/backend/internal/db/retention_policies.sql.go deleted file mode 100644 index 237efdd..0000000 --- a/backend/internal/db/retention_policies.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/storage_locations.sql.go b/backend/internal/db/storage_locations.sql.go deleted file mode 100644 index 6295366..0000000 --- a/backend/internal/db/storage_locations.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/sync_queue.sql.go b/backend/internal/db/sync_queue.sql.go deleted file mode 100644 index 507f97c..0000000 --- a/backend/internal/db/sync_queue.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/db/tags.sql.go b/backend/internal/db/tags.sql.go deleted file mode 100644 index 76941ef..0000000 --- a/backend/internal/db/tags.sql.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/handler/device.go b/backend/internal/handler/device.go deleted file mode 100644 index 632e4e2..0000000 --- a/backend/internal/handler/device.go +++ /dev/null @@ -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, - }) -} diff --git a/backend/internal/handler/events.go b/backend/internal/handler/events.go deleted file mode 100644 index f48b7b3..0000000 --- a/backend/internal/handler/events.go +++ /dev/null @@ -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 - }) -} diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go deleted file mode 100644 index bf22be4..0000000 --- a/backend/internal/handler/handler.go +++ /dev/null @@ -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), - } -} diff --git a/backend/internal/handler/health.go b/backend/internal/handler/health.go deleted file mode 100644 index be09a68..0000000 --- a/backend/internal/handler/health.go +++ /dev/null @@ -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, - }) -} diff --git a/backend/internal/handler/ocr.go b/backend/internal/handler/ocr.go deleted file mode 100644 index 3838cb5..0000000 --- a/backend/internal/handler/ocr.go +++ /dev/null @@ -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") -} diff --git a/backend/internal/handler/resource.go b/backend/internal/handler/resource.go deleted file mode 100644 index eab08a3..0000000 --- a/backend/internal/handler/resource.go +++ /dev/null @@ -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), - }) -} diff --git a/backend/internal/handler/router.go b/backend/internal/handler/router.go deleted file mode 100644 index 1531086..0000000 --- a/backend/internal/handler/router.go +++ /dev/null @@ -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) -} diff --git a/backend/internal/handler/share.go b/backend/internal/handler/share.go deleted file mode 100644 index 54bee37..0000000 --- a/backend/internal/handler/share.go +++ /dev/null @@ -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 != "", - }) -} diff --git a/backend/internal/handler/sync.go b/backend/internal/handler/sync.go deleted file mode 100644 index 686e7c3..0000000 --- a/backend/internal/handler/sync.go +++ /dev/null @@ -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", - }) -} diff --git a/backend/internal/model/file.go b/backend/internal/model/file.go deleted file mode 100644 index d4540af..0000000 --- a/backend/internal/model/file.go +++ /dev/null @@ -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"` -} diff --git a/backend/internal/model/ocrjob.go b/backend/internal/model/ocrjob.go deleted file mode 100644 index 435a6a0..0000000 --- a/backend/internal/model/ocrjob.go +++ /dev/null @@ -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"` -} diff --git a/backend/internal/ocr/client.go b/backend/internal/ocr/client.go deleted file mode 100644 index 3ab22a3..0000000 --- a/backend/internal/ocr/client.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/ocr/types.go b/backend/internal/ocr/types.go deleted file mode 100644 index 86874e3..0000000 --- a/backend/internal/ocr/types.go +++ /dev/null @@ -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"` -} diff --git a/backend/internal/service/checksum.go b/backend/internal/service/checksum.go deleted file mode 100644 index 3bcd0ef..0000000 --- a/backend/internal/service/checksum.go +++ /dev/null @@ -1,10 +0,0 @@ -package service - -import ( - "crypto/sha256" -) - -func CreateSHA256Hash(data []byte) []byte { - h := sha256.Sum256(data) - return h[:] -} diff --git a/backend/internal/service/conversion.go b/backend/internal/service/conversion.go deleted file mode 100644 index 17a8c37..0000000 --- a/backend/internal/service/conversion.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/service/events.go b/backend/internal/service/events.go deleted file mode 100644 index 8f676b3..0000000 --- a/backend/internal/service/events.go +++ /dev/null @@ -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: - } - } -} diff --git a/backend/internal/service/ocr.go b/backend/internal/service/ocr.go deleted file mode 100644 index 3994deb..0000000 --- a/backend/internal/service/ocr.go +++ /dev/null @@ -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) -} diff --git a/backend/internal/service/placement.go b/backend/internal/service/placement.go deleted file mode 100644 index 7252e67..0000000 --- a/backend/internal/service/placement.go +++ /dev/null @@ -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) -} - - diff --git a/backend/internal/service/rebac.go b/backend/internal/service/rebac.go deleted file mode 100644 index 938154c..0000000 --- a/backend/internal/service/rebac.go +++ /dev/null @@ -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) -} diff --git a/backend/internal/service/resource.go b/backend/internal/service/resource.go deleted file mode 100644 index 4b95f72..0000000 --- a/backend/internal/service/resource.go +++ /dev/null @@ -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 -} diff --git a/backend/internal/service/sync.go b/backend/internal/service/sync.go deleted file mode 100644 index 0d2615e..0000000 --- a/backend/internal/service/sync.go +++ /dev/null @@ -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, - }) -} diff --git a/backend/internal/service/url.go b/backend/internal/service/url.go deleted file mode 100644 index af868fc..0000000 --- a/backend/internal/service/url.go +++ /dev/null @@ -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)) -} diff --git a/backend/main_test.go b/backend/main_test.go new file mode 100644 index 0000000..5d3a0b5 --- /dev/null +++ b/backend/main_test.go @@ -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) + +} diff --git a/backend/ocr-server/Dockerfile b/backend/ocr-server/Dockerfile deleted file mode 100644 index 3314ae3..0000000 --- a/backend/ocr-server/Dockerfile +++ /dev/null @@ -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"] diff --git a/backend/ocr-server/server.py b/backend/ocr-server/server.py deleted file mode 100644 index c14fe4c..0000000 --- a/backend/ocr-server/server.py +++ /dev/null @@ -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)}, - ) diff --git a/backend/pkg/api/response.go b/backend/pkg/api/response.go deleted file mode 100644 index 18720b6..0000000 --- a/backend/pkg/api/response.go +++ /dev/null @@ -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, - }, - }) -} diff --git a/backend/sqlc.yaml b/backend/sqlc.yaml deleted file mode 100644 index 40f137b..0000000 --- a/backend/sqlc.yaml +++ /dev/null @@ -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 diff --git a/backend/uploads/.gitkeep b/backend/uploads/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/backend/users.go b/backend/users.go new file mode 100644 index 0000000..ac9485d --- /dev/null +++ b/backend/users.go @@ -0,0 +1,11 @@ +package main + +type User struct { + Username string +} + +func NewUser(username string) (error, User) { + return nil, User{ + Username: username, + } +}