create tests directory format

This commit is contained in:
m
2026-09-02 09:52:26 +02:00
parent 479c8ab61c
commit 32a61e6a64
94 changed files with 95 additions and 6476 deletions
-31
View File
@@ -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
-25
View File
@@ -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"]
-147
View File
@@ -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))
}
-63
View File
@@ -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)
}
}
+46
View File
@@ -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,
}
}
-135
View File
@@ -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"})
}
-41
View File
@@ -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()
}
}
-292
View File
@@ -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[:])
}
-57
View File
@@ -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
}
-137
View File
@@ -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
}
-31
View File
@@ -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
}
-31
View File
@@ -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,
}
}
-21
View File
@@ -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
}
-49
View File
@@ -1,49 +0,0 @@
package db
import (
"fmt"
"log"
"os"
"database/sql"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/file"
)
func RunMigrations(migrationsURL string) error {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "postgres://localhost:5432/vaultdrop?sslmode=disable"
}
migDB, err := sql.Open("postgres", dsn)
if err != nil {
return fmt.Errorf("open migration db: %w", err)
}
defer migDB.Close()
sourceDriver, err := (&file.File{}).Open(migrationsURL)
if err != nil {
return fmt.Errorf("open migrations source: %w", err)
}
dbDriver, err := postgres.WithInstance(migDB, &postgres.Config{})
if err != nil {
return fmt.Errorf("create postgres driver: %w", err)
}
m, err := migrate.NewWithInstance("file", sourceDriver, "postgres", dbDriver)
if err != nil {
return fmt.Errorf("create migrate instance: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("run migrations: %w", err)
}
log.Println("Database migrations applied successfully")
return nil
}
@@ -1 +0,0 @@
-- Rollback 001_init
@@ -1,3 +0,0 @@
-- VaultDrop 001: Initial schema
-- This is a placeholder. Add your first CREATE TABLE here.
SELECT 1;
@@ -1 +0,0 @@
DROP TABLE IF EXISTS files;
@@ -1,12 +0,0 @@
-- VaultDrop 002: Files table
CREATE TABLE files (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL,
checksum TEXT NOT NULL DEFAULT '',
ocr_text TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -1,3 +0,0 @@
DROP TABLE IF EXISTS file_tags;
DROP TABLE IF EXISTS tags;
@@ -1,19 +0,0 @@
-- VaultDrop 003: tags table
CREATE TABLE tags (
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
parent_tag_id TEXT,
tag_name TEXT NOT NULL, -- exemple: Brice, vélo, facture ...
tag_type TEXT NOT NULL DEFAULT 'none', -- exemple: entity, none
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_parent_tag FOREIGN KEY(parent_tag_id) REFERENCES tags(id)
);
CREATE TABLE file_tags (
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
tag_id TEXT,
file_id TEXT,
CONSTRAINT fk_tag FOREIGN KEY(tag_id) REFERENCES tags(id),
CONSTRAINT fk_file FOREIGN KEY(file_id) REFERENCES files(id)
)
@@ -1,2 +0,0 @@
-- VaultDrop 003: tags table
ALTER TABLE files DROP COLUMN IF EXISTS is_folder;
@@ -1 +0,0 @@
ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false;
@@ -1,3 +0,0 @@
ALTER TABLE files DROP CONSTRAINT IF EXISTS fk_parent_file_id
ALTER TABLE files DROP COLUMN IF EXISTS parent_file_id;
@@ -1,3 +0,0 @@
ALTER TABLE files ADD COLUMN parent_file_id TEXT;
ALTER TABLE files ADD CONSTRAINT fk_parent_file_id FOREIGN KEY(parent_file_id) REFERENCES files(id)
@@ -1 +0,0 @@
ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false;
@@ -1,2 +0,0 @@
-- VaultDrop 003: tags table
ALTER TABLE files DROP COLUMN IF EXISTS is_folder;
@@ -1 +0,0 @@
ALTER TABLE files DROP COLUMN IF EXISTS is_folder;
@@ -1,2 +0,0 @@
-- VaultDrop 007: re-add is_folder to distinguish folders from files
ALTER TABLE files ADD COLUMN is_folder BOOLEAN NOT NULL DEFAULT false;
@@ -1 +0,0 @@
ALTER TABLE files ALTER COLUMN id DROP DEFAULT;
@@ -1,2 +0,0 @@
ALTER TABLE files ALTER COLUMN id SET DEFAULT gen_random_uuid();
ALTER TABLE files ALTER COLUMN storage_key SET DEFAULT '';
@@ -1,5 +0,0 @@
CREATE TABLE users (
id TEXT NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -1 +0,0 @@
DROP TABLE IF EXISTS users;
@@ -1,2 +0,0 @@
DROP TABLE IF EXISTS refresh_tokens;
DROP TABLE IF EXISTS users;
@@ -1,19 +0,0 @@
CREATE TABLE users (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE refresh_tokens (
id TEXT NOT NULL DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
@@ -1 +0,0 @@
DROP TABLE IF EXISTS thumbnails;
@@ -1,14 +0,0 @@
CREATE TABLE thumbnails (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
page_number INTEGER NOT NULL,
resolution_label TEXT NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
storage_key TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_thumbnails_file_id ON thumbnails(file_id);
CREATE UNIQUE INDEX idx_thumbnails_unique ON thumbnails(file_id, page_number, resolution_label);
@@ -1,73 +0,0 @@
-- VaultDrop 012 down: revert to V1 schema
DROP FUNCTION IF EXISTS resolve_effective_role;
DROP TABLE IF EXISTS sync_queue CASCADE;
DROP TABLE IF EXISTS retention_policies CASCADE;
DROP TABLE IF EXISTS rebac_relations CASCADE;
DROP TABLE IF EXISTS resource_placements CASCADE;
DROP TABLE IF EXISTS resource_variants CASCADE;
DROP TABLE IF EXISTS resource_tags CASCADE;
DROP TABLE IF EXISTS refresh_tokens CASCADE;
DROP TABLE IF EXISTS storage_locations CASCADE;
DROP TABLE IF EXISTS resources CASCADE;
DROP TABLE IF EXISTS tags CASCADE;
DROP TABLE IF EXISTS users CASCADE;
-- Restore V1 tables
CREATE TABLE users (
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
parent_tag_id TEXT,
tag_name TEXT NOT NULL,
tag_type TEXT NOT NULL DEFAULT 'none',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE files (
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL DEFAULT '',
checksum TEXT NOT NULL DEFAULT '',
ocr_text TEXT NOT NULL DEFAULT '',
is_folder BOOLEAN NOT NULL DEFAULT false,
parent_file_id TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE file_tags (
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
tag_id TEXT,
file_id TEXT
);
CREATE TABLE thumbnails (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
page_number INTEGER NOT NULL,
resolution_label TEXT NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
storage_key TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE refresh_tokens (
id TEXT NOT NULL DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -1,187 +0,0 @@
-- VaultDrop 012: Clean V3 schema
-- Drops all legacy V1 tables, recreates everything with UUIDs
-- Drop legacy tables (order matters for FK dependencies)
DROP TABLE IF EXISTS file_tags CASCADE;
DROP TABLE IF EXISTS resource_tags CASCADE;
DROP TABLE IF EXISTS thumbnails CASCADE;
DROP TABLE IF EXISTS refresh_tokens CASCADE;
DROP TABLE IF EXISTS files CASCADE;
DROP TABLE IF EXISTS tags CASCADE;
DROP TABLE IF EXISTS users CASCADE;
-- Level 1: No dependencies
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
parent_user_id UUID REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_tag_id UUID REFERENCES tags(id),
tag_name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Level 2: Depend on users
CREATE TABLE resources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
checksum TEXT NOT NULL DEFAULT '',
ocr_text TEXT NOT NULL DEFAULT '',
is_folder BOOLEAN NOT NULL DEFAULT false,
parent_resource_id UUID REFERENCES resources(id),
owner_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_resources_owner ON resources(owner_id);
CREATE INDEX idx_resources_parent ON resources(parent_resource_id);
CREATE INDEX idx_resources_checksum_owner ON resources(checksum, owner_id);
CREATE TABLE storage_locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
device_name TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('primary', 'device', 'backup', 'server')),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_seen_at TIMESTAMP
);
CREATE INDEX idx_locations_user ON storage_locations(user_id);
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
-- Level 3: Depend on level 1-2
CREATE TABLE resource_tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tag_id UUID NOT NULL REFERENCES tags(id),
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
UNIQUE(tag_id, resource_id)
);
CREATE INDEX idx_resource_tags_tag ON resource_tags(tag_id);
CREATE INDEX idx_resource_tags_resource ON resource_tags(resource_id);
CREATE TABLE resource_variants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
variant_type TEXT NOT NULL,
page_number INTEGER NOT NULL DEFAULT 1,
width INTEGER NOT NULL DEFAULT 0,
height INTEGER NOT NULL DEFAULT 0,
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
generated_by TEXT NOT NULL DEFAULT 'server' CHECK (generated_by IN ('server', 'client')),
storage_key TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_variants_resource ON resource_variants(resource_id);
CREATE UNIQUE INDEX idx_variants_resource_type_page ON resource_variants(resource_id, variant_type, page_number);
CREATE TABLE resource_placements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
storage_location_id UUID NOT NULL REFERENCES storage_locations(id),
status TEXT NOT NULL DEFAULT 'synced' CHECK (status IN ('local_only', 'synced', 'cloud_only', 'pending_upload', 'pending_download')),
storage_key TEXT,
synced_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(resource_id, storage_location_id)
);
CREATE INDEX idx_placements_resource ON resource_placements(resource_id);
CREATE INDEX idx_placements_location ON resource_placements(storage_location_id);
CREATE INDEX idx_placements_status ON resource_placements(status);
CREATE TABLE rebac_relations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
subject_user_id UUID NOT NULL REFERENCES users(id),
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'editor', 'viewer')),
granted_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(resource_id, subject_user_id)
);
CREATE INDEX idx_rebac_resource ON rebac_relations(resource_id);
CREATE INDEX idx_rebac_subject ON rebac_relations(subject_user_id);
CREATE TABLE retention_policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
storage_location_id UUID NOT NULL REFERENCES storage_locations(id),
rule_type TEXT NOT NULL,
rule_value JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_policies_user ON retention_policies(user_id);
CREATE INDEX idx_policies_location ON retention_policies(storage_location_id);
CREATE TABLE sync_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
storage_location_id UUID NOT NULL REFERENCES storage_locations(id),
operation TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_queue_status ON sync_queue(status);
CREATE INDEX idx_queue_resource ON sync_queue(resource_id);
CREATE INDEX idx_queue_location ON sync_queue(storage_location_id);
-- ReBAC permission resolver
CREATE OR REPLACE FUNCTION resolve_effective_role(p_user_id UUID, p_resource_id UUID)
RETURNS TEXT AS $$
DECLARE
v_role TEXT;
BEGIN
WITH RECURSIVE rtree AS (
SELECT r.id, r.parent_resource_id, r.owner_id
FROM resources r
WHERE r.id = p_resource_id
UNION ALL
SELECT r.id, r.parent_resource_id, r.owner_id
FROM resources r
JOIN rtree ON r.id = rtree.parent_resource_id
)
SELECT CASE
WHEN EXISTS(SELECT 1 FROM rtree WHERE owner_id = p_user_id) THEN 'owner'
ELSE COALESCE(
(SELECT rr.role::text FROM rebac_relations rr
JOIN rtree ON rr.resource_id = rtree.id
WHERE rr.subject_user_id = p_user_id
ORDER BY CASE rr.role
WHEN 'owner' THEN 0
WHEN 'admin' THEN 1
WHEN 'editor' THEN 2
WHEN 'viewer' THEN 3
END ASC LIMIT 1),
''
)
END INTO v_role;
RETURN v_role;
END;
$$ LANGUAGE plpgsql;
@@ -1 +0,0 @@
DROP INDEX IF EXISTS idx_storage_locations_user_server;
@@ -1,2 +0,0 @@
CREATE UNIQUE INDEX IF NOT EXISTS idx_storage_locations_user_server
ON storage_locations(user_id) WHERE role = 'server';
@@ -1 +0,0 @@
DROP TABLE IF EXISTS ocr_jobs;
@@ -1,12 +0,0 @@
CREATE TABLE ocr_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
file_path TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'done', 'failed')),
error_message TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_ocr_jobs_status ON ocr_jobs(status);
CREATE INDEX idx_ocr_jobs_resource ON ocr_jobs(resource_id);
@@ -1,2 +0,0 @@
ALTER TABLE resource_tags DROP CONSTRAINT resource_tags_resource_id_fkey,
ADD CONSTRAINT resource_tags_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES resources(id);
@@ -1,2 +0,0 @@
ALTER TABLE resource_tags DROP CONSTRAINT resource_tags_resource_id_fkey,
ADD CONSTRAINT resource_tags_resource_id_fkey FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE;
-130
View File
@@ -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"`
}
-122
View File
@@ -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
}
-25
View File
@@ -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;
-2
View File
@@ -1,2 +0,0 @@
-- name: GetHealth :one
SELECT 1 AS ok;
-22
View File
@@ -1,22 +0,0 @@
-- name: CreateOCRJob :one
INSERT INTO ocr_jobs (resource_id, file_path, status)
VALUES ($1, $2, 'pending')
RETURNING *;
-- name: GetOCRJob :one
SELECT * FROM ocr_jobs
WHERE id = $1 LIMIT 1;
-- name: ListPendingOCRJobs :many
SELECT * FROM ocr_jobs
WHERE status = 'pending'
ORDER BY created_at ASC;
-- name: UpdateOCRJobStatus :exec
UPDATE ocr_jobs
SET status = $1, error_message = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $3;
-- name: DeleteOCRJob :exec
DELETE FROM ocr_jobs
WHERE id = $1;
@@ -1,36 +0,0 @@
-- name: CreateRebacRelation :one
INSERT INTO rebac_relations (resource_id, subject_user_id, role, granted_by)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetRebacRelation :one
SELECT * FROM rebac_relations
WHERE resource_id = $1 AND subject_user_id = $2
LIMIT 1;
-- name: ListRebacRelationsByResource :many
SELECT * FROM rebac_relations
WHERE resource_id = $1
ORDER BY created_at ASC;
-- name: ListRebacRelationsBySubject :many
SELECT * FROM rebac_relations
WHERE subject_user_id = $1
ORDER BY created_at ASC;
-- name: DeleteRebacRelation :exec
DELETE FROM rebac_relations
WHERE resource_id = $1 AND subject_user_id = $2;
-- name: DeleteRebacRelationsByResource :exec
DELETE FROM rebac_relations
WHERE resource_id = $1;
-- name: HasRebacRelation :one
SELECT EXISTS(
SELECT 1 FROM rebac_relations
WHERE resource_id = $1 AND subject_user_id = $2 AND role = $3
);
-- name: ResolveEffectiveRole :one
SELECT resolve_effective_role($1, $2) AS role;
@@ -1,32 +0,0 @@
-- name: CreatePlacement :one
INSERT INTO resource_placements (resource_id, storage_location_id, status, storage_key, synced_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: GetPlacement :one
SELECT * FROM resource_placements
WHERE resource_id = $1 AND storage_location_id = $2
LIMIT 1;
-- name: ListPlacementsByResource :many
SELECT * FROM resource_placements
WHERE resource_id = $1;
-- name: ListPlacementsByLocation :many
SELECT * FROM resource_placements
WHERE storage_location_id = $1;
-- name: UpdatePlacementStatus :exec
UPDATE resource_placements
SET status = $1, synced_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: DeletePlacement :exec
DELETE FROM resource_placements
WHERE id = $1;
-- name: GetServerPlacementByResource :one
SELECT rp.* FROM resource_placements rp
JOIN storage_locations sl ON sl.id = rp.storage_location_id
WHERE rp.resource_id = $1 AND sl.role = 'server'
LIMIT 1;
@@ -1,21 +0,0 @@
-- name: CreateResourceVariant :one
INSERT INTO resource_variants (resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: GetVariantsByResourceID :many
SELECT * FROM resource_variants
WHERE resource_id = $1
ORDER BY page_number ASC, variant_type ASC;
-- name: GetVariantByID :one
SELECT * FROM resource_variants
WHERE id = $1 LIMIT 1;
-- name: DeleteVariantsByResourceID :exec
DELETE FROM resource_variants WHERE resource_id = $1;
-- name: GetBestVariant :one
SELECT * FROM resource_variants
WHERE resource_id = $1 AND variant_type = $2 AND page_number = 1
LIMIT 1;
-77
View File
@@ -1,77 +0,0 @@
-- name: GetResource :one
SELECT * FROM resources
WHERE id = $1 LIMIT 1;
-- name: ListResources :many
SELECT * FROM resources
WHERE parent_resource_id IS NULL
ORDER BY is_folder DESC, created_at DESC;
-- name: ListFolders :many
SELECT * FROM resources
WHERE is_folder = true
ORDER BY created_at DESC;
-- name: ListResourcesByID :many
SELECT * FROM resources
WHERE id = ANY($1::uuid[])
ORDER BY created_at DESC;
-- name: CreateResource :one
INSERT INTO resources (name, mime_type, size, checksum, owner_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING *;
-- name: CreateFolder :one
INSERT INTO resources (name, is_folder, owner_id, parent_resource_id, created_at, updated_at)
VALUES ($1, true, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING *;
-- name: ListResourcesByParentID :many
SELECT * FROM resources
WHERE parent_resource_id = $1
ORDER BY is_folder DESC, created_at DESC;
-- name: MoveResources :exec
UPDATE resources
SET parent_resource_id = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = ANY($2::uuid[]);
-- name: UpdateResource :exec
UPDATE resources
SET name = $1, mime_type = $2, ocr_text = $3, updated_at = CURRENT_TIMESTAMP
WHERE id = $4;
-- name: DeleteResource :exec
DELETE FROM resources
WHERE id = $1;
-- name: FindDuplicatesByNameSize :many
SELECT id, name, mime_type, size, checksum, created_at FROM resources
WHERE name = $1 AND size = $2 AND is_folder = false
ORDER BY created_at DESC;
-- name: FindDuplicateByChecksum :one
SELECT * FROM resources
WHERE checksum = $1 AND is_folder = false AND owner_id = $2
LIMIT 1;
-- name: CountResourcesByOwner :one
SELECT COUNT(*) FROM resources
WHERE owner_id = $1 AND parent_resource_id IS NULL;
-- name: ListResourcesByOwner :many
SELECT * FROM resources
WHERE owner_id = $1 AND parent_resource_id IS NULL
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CountResourcesByParentAndOwner :one
SELECT COUNT(*) FROM resources
WHERE parent_resource_id = $1 AND owner_id = $2;
-- name: ListResourcesByParentAndOwner :many
SELECT * FROM resources
WHERE parent_resource_id = $1 AND owner_id = $2
ORDER BY is_folder DESC, created_at DESC
LIMIT $3 OFFSET $4;
@@ -1,20 +0,0 @@
-- name: CreateRetentionPolicy :one
INSERT INTO retention_policies (user_id, storage_location_id, rule_type, rule_value)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetRetentionPolicy :one
SELECT * FROM retention_policies
WHERE id = $1;
-- name: ListRetentionPoliciesByUser :many
SELECT * FROM retention_policies
WHERE user_id = $1;
-- name: ListRetentionPoliciesByLocation :many
SELECT * FROM retention_policies
WHERE storage_location_id = $1;
-- name: DeleteRetentionPolicy :exec
DELETE FROM retention_policies
WHERE id = $1;
@@ -1,27 +0,0 @@
-- name: CreateStorageLocation :one
INSERT INTO storage_locations (user_id, device_name, role)
VALUES ($1, $2, $3)
RETURNING *;
-- name: GetStorageLocation :one
SELECT * FROM storage_locations
WHERE id = $1;
-- name: ListStorageLocationsByUser :many
SELECT * FROM storage_locations
WHERE user_id = $1
ORDER BY created_at ASC;
-- name: GetServerStorageLocation :one
SELECT * FROM storage_locations
WHERE user_id = $1 AND role = 'server'
LIMIT 1;
-- name: UpdateStorageLocationLastSeen :exec
UPDATE storage_locations
SET last_seen_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: DeleteStorageLocation :exec
DELETE FROM storage_locations
WHERE id = $1;
@@ -1,27 +0,0 @@
-- name: CreateSyncQueueItem :one
INSERT INTO sync_queue (resource_id, storage_location_id, operation, status, attempts)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- name: GetSyncQueueItem :one
SELECT * FROM sync_queue
WHERE id = $1;
-- name: ListPendingSyncItems :many
SELECT * FROM sync_queue
WHERE status = 'pending'
ORDER BY created_at ASC;
-- name: ListPendingSyncItemsByLocation :many
SELECT * FROM sync_queue
WHERE storage_location_id = $1 AND status = 'pending'
ORDER BY created_at ASC;
-- name: UpdateSyncQueueStatus :exec
UPDATE sync_queue
SET status = $1, attempts = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $3;
-- name: DeleteSyncQueueItem :exec
DELETE FROM sync_queue
WHERE id = $1;
-41
View File
@@ -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;
-202
View File
@@ -1,202 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: rebac_relations.sql
package db
import (
"context"
"github.com/google/uuid"
)
const createRebacRelation = `-- name: CreateRebacRelation :one
INSERT INTO rebac_relations (resource_id, subject_user_id, role, granted_by)
VALUES ($1, $2, $3, $4)
RETURNING id, resource_id, subject_user_id, role, granted_by, created_at
`
type CreateRebacRelationParams struct {
ResourceID uuid.UUID `json:"resource_id"`
SubjectUserID uuid.UUID `json:"subject_user_id"`
Role string `json:"role"`
GrantedBy uuid.UUID `json:"granted_by"`
}
func (q *Queries) CreateRebacRelation(ctx context.Context, arg CreateRebacRelationParams) (RebacRelation, error) {
row := q.db.QueryRowContext(ctx, createRebacRelation,
arg.ResourceID,
arg.SubjectUserID,
arg.Role,
arg.GrantedBy,
)
var i RebacRelation
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.SubjectUserID,
&i.Role,
&i.GrantedBy,
&i.CreatedAt,
)
return i, err
}
const deleteRebacRelation = `-- name: DeleteRebacRelation :exec
DELETE FROM rebac_relations
WHERE resource_id = $1 AND subject_user_id = $2
`
type DeleteRebacRelationParams struct {
ResourceID uuid.UUID `json:"resource_id"`
SubjectUserID uuid.UUID `json:"subject_user_id"`
}
func (q *Queries) DeleteRebacRelation(ctx context.Context, arg DeleteRebacRelationParams) error {
_, err := q.db.ExecContext(ctx, deleteRebacRelation, arg.ResourceID, arg.SubjectUserID)
return err
}
const deleteRebacRelationsByResource = `-- name: DeleteRebacRelationsByResource :exec
DELETE FROM rebac_relations
WHERE resource_id = $1
`
func (q *Queries) DeleteRebacRelationsByResource(ctx context.Context, resourceID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteRebacRelationsByResource, resourceID)
return err
}
const getRebacRelation = `-- name: GetRebacRelation :one
SELECT id, resource_id, subject_user_id, role, granted_by, created_at FROM rebac_relations
WHERE resource_id = $1 AND subject_user_id = $2
LIMIT 1
`
type GetRebacRelationParams struct {
ResourceID uuid.UUID `json:"resource_id"`
SubjectUserID uuid.UUID `json:"subject_user_id"`
}
func (q *Queries) GetRebacRelation(ctx context.Context, arg GetRebacRelationParams) (RebacRelation, error) {
row := q.db.QueryRowContext(ctx, getRebacRelation, arg.ResourceID, arg.SubjectUserID)
var i RebacRelation
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.SubjectUserID,
&i.Role,
&i.GrantedBy,
&i.CreatedAt,
)
return i, err
}
const hasRebacRelation = `-- name: HasRebacRelation :one
SELECT EXISTS(
SELECT 1 FROM rebac_relations
WHERE resource_id = $1 AND subject_user_id = $2 AND role = $3
)
`
type HasRebacRelationParams struct {
ResourceID uuid.UUID `json:"resource_id"`
SubjectUserID uuid.UUID `json:"subject_user_id"`
Role string `json:"role"`
}
func (q *Queries) HasRebacRelation(ctx context.Context, arg HasRebacRelationParams) (bool, error) {
row := q.db.QueryRowContext(ctx, hasRebacRelation, arg.ResourceID, arg.SubjectUserID, arg.Role)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const listRebacRelationsByResource = `-- name: ListRebacRelationsByResource :many
SELECT id, resource_id, subject_user_id, role, granted_by, created_at FROM rebac_relations
WHERE resource_id = $1
ORDER BY created_at ASC
`
func (q *Queries) ListRebacRelationsByResource(ctx context.Context, resourceID uuid.UUID) ([]RebacRelation, error) {
rows, err := q.db.QueryContext(ctx, listRebacRelationsByResource, resourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RebacRelation
for rows.Next() {
var i RebacRelation
if err := rows.Scan(
&i.ID,
&i.ResourceID,
&i.SubjectUserID,
&i.Role,
&i.GrantedBy,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRebacRelationsBySubject = `-- name: ListRebacRelationsBySubject :many
SELECT id, resource_id, subject_user_id, role, granted_by, created_at FROM rebac_relations
WHERE subject_user_id = $1
ORDER BY created_at ASC
`
func (q *Queries) ListRebacRelationsBySubject(ctx context.Context, subjectUserID uuid.UUID) ([]RebacRelation, error) {
rows, err := q.db.QueryContext(ctx, listRebacRelationsBySubject, subjectUserID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RebacRelation
for rows.Next() {
var i RebacRelation
if err := rows.Scan(
&i.ID,
&i.ResourceID,
&i.SubjectUserID,
&i.Role,
&i.GrantedBy,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const resolveEffectiveRole = `-- name: ResolveEffectiveRole :one
SELECT resolve_effective_role($1, $2) AS role
`
type ResolveEffectiveRoleParams struct {
PUserID uuid.UUID `json:"p_user_id"`
PResourceID uuid.UUID `json:"p_resource_id"`
}
func (q *Queries) ResolveEffectiveRole(ctx context.Context, arg ResolveEffectiveRoleParams) (string, error) {
row := q.db.QueryRowContext(ctx, resolveEffectiveRole, arg.PUserID, arg.PResourceID)
var role string
err := row.Scan(&role)
return role, err
}
@@ -1,194 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: resource_placements.sql
package db
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const createPlacement = `-- name: CreatePlacement :one
INSERT INTO resource_placements (resource_id, storage_location_id, status, storage_key, synced_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, resource_id, storage_location_id, status, storage_key, synced_at, created_at
`
type CreatePlacementParams struct {
ResourceID uuid.UUID `json:"resource_id"`
StorageLocationID uuid.UUID `json:"storage_location_id"`
Status string `json:"status"`
StorageKey sql.NullString `json:"storage_key"`
SyncedAt sql.NullTime `json:"synced_at"`
}
func (q *Queries) CreatePlacement(ctx context.Context, arg CreatePlacementParams) (ResourcePlacement, error) {
row := q.db.QueryRowContext(ctx, createPlacement,
arg.ResourceID,
arg.StorageLocationID,
arg.Status,
arg.StorageKey,
arg.SyncedAt,
)
var i ResourcePlacement
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.StorageLocationID,
&i.Status,
&i.StorageKey,
&i.SyncedAt,
&i.CreatedAt,
)
return i, err
}
const deletePlacement = `-- name: DeletePlacement :exec
DELETE FROM resource_placements
WHERE id = $1
`
func (q *Queries) DeletePlacement(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deletePlacement, id)
return err
}
const getPlacement = `-- name: GetPlacement :one
SELECT id, resource_id, storage_location_id, status, storage_key, synced_at, created_at FROM resource_placements
WHERE resource_id = $1 AND storage_location_id = $2
LIMIT 1
`
type GetPlacementParams struct {
ResourceID uuid.UUID `json:"resource_id"`
StorageLocationID uuid.UUID `json:"storage_location_id"`
}
func (q *Queries) GetPlacement(ctx context.Context, arg GetPlacementParams) (ResourcePlacement, error) {
row := q.db.QueryRowContext(ctx, getPlacement, arg.ResourceID, arg.StorageLocationID)
var i ResourcePlacement
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.StorageLocationID,
&i.Status,
&i.StorageKey,
&i.SyncedAt,
&i.CreatedAt,
)
return i, err
}
const getServerPlacementByResource = `-- name: GetServerPlacementByResource :one
SELECT rp.id, rp.resource_id, rp.storage_location_id, rp.status, rp.storage_key, rp.synced_at, rp.created_at FROM resource_placements rp
JOIN storage_locations sl ON sl.id = rp.storage_location_id
WHERE rp.resource_id = $1 AND sl.role = 'server'
LIMIT 1
`
func (q *Queries) GetServerPlacementByResource(ctx context.Context, resourceID uuid.UUID) (ResourcePlacement, error) {
row := q.db.QueryRowContext(ctx, getServerPlacementByResource, resourceID)
var i ResourcePlacement
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.StorageLocationID,
&i.Status,
&i.StorageKey,
&i.SyncedAt,
&i.CreatedAt,
)
return i, err
}
const listPlacementsByLocation = `-- name: ListPlacementsByLocation :many
SELECT id, resource_id, storage_location_id, status, storage_key, synced_at, created_at FROM resource_placements
WHERE storage_location_id = $1
`
func (q *Queries) ListPlacementsByLocation(ctx context.Context, storageLocationID uuid.UUID) ([]ResourcePlacement, error) {
rows, err := q.db.QueryContext(ctx, listPlacementsByLocation, storageLocationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ResourcePlacement
for rows.Next() {
var i ResourcePlacement
if err := rows.Scan(
&i.ID,
&i.ResourceID,
&i.StorageLocationID,
&i.Status,
&i.StorageKey,
&i.SyncedAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlacementsByResource = `-- name: ListPlacementsByResource :many
SELECT id, resource_id, storage_location_id, status, storage_key, synced_at, created_at FROM resource_placements
WHERE resource_id = $1
`
func (q *Queries) ListPlacementsByResource(ctx context.Context, resourceID uuid.UUID) ([]ResourcePlacement, error) {
rows, err := q.db.QueryContext(ctx, listPlacementsByResource, resourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ResourcePlacement
for rows.Next() {
var i ResourcePlacement
if err := rows.Scan(
&i.ID,
&i.ResourceID,
&i.StorageLocationID,
&i.Status,
&i.StorageKey,
&i.SyncedAt,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updatePlacementStatus = `-- name: UpdatePlacementStatus :exec
UPDATE resource_placements
SET status = $1, synced_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdatePlacementStatusParams struct {
Status string `json:"status"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdatePlacementStatus(ctx context.Context, arg UpdatePlacementStatusParams) error {
_, err := q.db.ExecContext(ctx, updatePlacementStatus, arg.Status, arg.ID)
return err
}
@@ -1,157 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: resource_variants.sql
package db
import (
"context"
"github.com/google/uuid"
)
const createResourceVariant = `-- name: CreateResourceVariant :one
INSERT INTO resource_variants (resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at
`
type CreateResourceVariantParams struct {
ResourceID uuid.UUID `json:"resource_id"`
VariantType string `json:"variant_type"`
PageNumber int32 `json:"page_number"`
Width int32 `json:"width"`
Height int32 `json:"height"`
MimeType string `json:"mime_type"`
GeneratedBy string `json:"generated_by"`
StorageKey string `json:"storage_key"`
}
func (q *Queries) CreateResourceVariant(ctx context.Context, arg CreateResourceVariantParams) (ResourceVariant, error) {
row := q.db.QueryRowContext(ctx, createResourceVariant,
arg.ResourceID,
arg.VariantType,
arg.PageNumber,
arg.Width,
arg.Height,
arg.MimeType,
arg.GeneratedBy,
arg.StorageKey,
)
var i ResourceVariant
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.VariantType,
&i.PageNumber,
&i.Width,
&i.Height,
&i.MimeType,
&i.GeneratedBy,
&i.StorageKey,
&i.CreatedAt,
)
return i, err
}
const deleteVariantsByResourceID = `-- name: DeleteVariantsByResourceID :exec
DELETE FROM resource_variants WHERE resource_id = $1
`
func (q *Queries) DeleteVariantsByResourceID(ctx context.Context, resourceID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteVariantsByResourceID, resourceID)
return err
}
const getBestVariant = `-- name: GetBestVariant :one
SELECT id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at FROM resource_variants
WHERE resource_id = $1 AND variant_type = $2 AND page_number = 1
LIMIT 1
`
type GetBestVariantParams struct {
ResourceID uuid.UUID `json:"resource_id"`
VariantType string `json:"variant_type"`
}
func (q *Queries) GetBestVariant(ctx context.Context, arg GetBestVariantParams) (ResourceVariant, error) {
row := q.db.QueryRowContext(ctx, getBestVariant, arg.ResourceID, arg.VariantType)
var i ResourceVariant
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.VariantType,
&i.PageNumber,
&i.Width,
&i.Height,
&i.MimeType,
&i.GeneratedBy,
&i.StorageKey,
&i.CreatedAt,
)
return i, err
}
const getVariantByID = `-- name: GetVariantByID :one
SELECT id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at FROM resource_variants
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetVariantByID(ctx context.Context, id uuid.UUID) (ResourceVariant, error) {
row := q.db.QueryRowContext(ctx, getVariantByID, id)
var i ResourceVariant
err := row.Scan(
&i.ID,
&i.ResourceID,
&i.VariantType,
&i.PageNumber,
&i.Width,
&i.Height,
&i.MimeType,
&i.GeneratedBy,
&i.StorageKey,
&i.CreatedAt,
)
return i, err
}
const getVariantsByResourceID = `-- name: GetVariantsByResourceID :many
SELECT id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at FROM resource_variants
WHERE resource_id = $1
ORDER BY page_number ASC, variant_type ASC
`
func (q *Queries) GetVariantsByResourceID(ctx context.Context, resourceID uuid.UUID) ([]ResourceVariant, error) {
rows, err := q.db.QueryContext(ctx, getVariantsByResourceID, resourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ResourceVariant
for rows.Next() {
var i ResourceVariant
if err := rows.Scan(
&i.ID,
&i.ResourceID,
&i.VariantType,
&i.PageNumber,
&i.Width,
&i.Height,
&i.MimeType,
&i.GeneratedBy,
&i.StorageKey,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
-532
View File
@@ -1,532 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: resources.sql
package db
import (
"context"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
)
const countResourcesByOwner = `-- name: CountResourcesByOwner :one
SELECT COUNT(*) FROM resources
WHERE owner_id = $1 AND parent_resource_id IS NULL
`
func (q *Queries) CountResourcesByOwner(ctx context.Context, ownerID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countResourcesByOwner, ownerID)
var count int64
err := row.Scan(&count)
return count, err
}
const countResourcesByParentAndOwner = `-- name: CountResourcesByParentAndOwner :one
SELECT COUNT(*) FROM resources
WHERE parent_resource_id = $1 AND owner_id = $2
`
type CountResourcesByParentAndOwnerParams struct {
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) CountResourcesByParentAndOwner(ctx context.Context, arg CountResourcesByParentAndOwnerParams) (int64, error) {
row := q.db.QueryRowContext(ctx, countResourcesByParentAndOwner, arg.ParentResourceID, arg.OwnerID)
var count int64
err := row.Scan(&count)
return count, err
}
const createFolder = `-- name: CreateFolder :one
INSERT INTO resources (name, is_folder, owner_id, parent_resource_id, created_at, updated_at)
VALUES ($1, true, $2, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at
`
type CreateFolderParams struct {
Name string `json:"name"`
OwnerID uuid.UUID `json:"owner_id"`
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
}
func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (Resource, error) {
row := q.db.QueryRowContext(ctx, createFolder, arg.Name, arg.OwnerID, arg.ParentResourceID)
var i Resource
err := row.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const createResource = `-- name: CreateResource :one
INSERT INTO resources (name, mime_type, size, checksum, owner_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at
`
type CreateResourceParams struct {
Name string `json:"name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Checksum string `json:"checksum"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) CreateResource(ctx context.Context, arg CreateResourceParams) (Resource, error) {
row := q.db.QueryRowContext(ctx, createResource,
arg.Name,
arg.MimeType,
arg.Size,
arg.Checksum,
arg.OwnerID,
)
var i Resource
err := row.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteResource = `-- name: DeleteResource :exec
DELETE FROM resources
WHERE id = $1
`
func (q *Queries) DeleteResource(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteResource, id)
return err
}
const findDuplicateByChecksum = `-- name: FindDuplicateByChecksum :one
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE checksum = $1 AND is_folder = false AND owner_id = $2
LIMIT 1
`
type FindDuplicateByChecksumParams struct {
Checksum string `json:"checksum"`
OwnerID uuid.UUID `json:"owner_id"`
}
func (q *Queries) FindDuplicateByChecksum(ctx context.Context, arg FindDuplicateByChecksumParams) (Resource, error) {
row := q.db.QueryRowContext(ctx, findDuplicateByChecksum, arg.Checksum, arg.OwnerID)
var i Resource
err := row.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const findDuplicatesByNameSize = `-- name: FindDuplicatesByNameSize :many
SELECT id, name, mime_type, size, checksum, created_at FROM resources
WHERE name = $1 AND size = $2 AND is_folder = false
ORDER BY created_at DESC
`
type FindDuplicatesByNameSizeParams struct {
Name string `json:"name"`
Size int64 `json:"size"`
}
type FindDuplicatesByNameSizeRow struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Checksum string `json:"checksum"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) FindDuplicatesByNameSize(ctx context.Context, arg FindDuplicatesByNameSizeParams) ([]FindDuplicatesByNameSizeRow, error) {
rows, err := q.db.QueryContext(ctx, findDuplicatesByNameSize, arg.Name, arg.Size)
if err != nil {
return nil, err
}
defer rows.Close()
var items []FindDuplicatesByNameSizeRow
for rows.Next() {
var i FindDuplicatesByNameSizeRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getResource = `-- name: GetResource :one
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetResource(ctx context.Context, id uuid.UUID) (Resource, error) {
row := q.db.QueryRowContext(ctx, getResource, id)
var i Resource
err := row.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listFolders = `-- name: ListFolders :many
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE is_folder = true
ORDER BY created_at DESC
`
func (q *Queries) ListFolders(ctx context.Context) ([]Resource, error) {
rows, err := q.db.QueryContext(ctx, listFolders)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Resource
for rows.Next() {
var i Resource
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listResources = `-- name: ListResources :many
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE parent_resource_id IS NULL
ORDER BY is_folder DESC, created_at DESC
`
func (q *Queries) ListResources(ctx context.Context) ([]Resource, error) {
rows, err := q.db.QueryContext(ctx, listResources)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Resource
for rows.Next() {
var i Resource
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listResourcesByID = `-- name: ListResourcesByID :many
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE id = ANY($1::uuid[])
ORDER BY created_at DESC
`
func (q *Queries) ListResourcesByID(ctx context.Context, dollar_1 []uuid.UUID) ([]Resource, error) {
rows, err := q.db.QueryContext(ctx, listResourcesByID, pq.Array(dollar_1))
if err != nil {
return nil, err
}
defer rows.Close()
var items []Resource
for rows.Next() {
var i Resource
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listResourcesByOwner = `-- name: ListResourcesByOwner :many
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE owner_id = $1 AND parent_resource_id IS NULL
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
type ListResourcesByOwnerParams struct {
OwnerID uuid.UUID `json:"owner_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListResourcesByOwner(ctx context.Context, arg ListResourcesByOwnerParams) ([]Resource, error) {
rows, err := q.db.QueryContext(ctx, listResourcesByOwner, arg.OwnerID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Resource
for rows.Next() {
var i Resource
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listResourcesByParentAndOwner = `-- name: ListResourcesByParentAndOwner :many
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE parent_resource_id = $1 AND owner_id = $2
ORDER BY is_folder DESC, created_at DESC
LIMIT $3 OFFSET $4
`
type ListResourcesByParentAndOwnerParams struct {
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
OwnerID uuid.UUID `json:"owner_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListResourcesByParentAndOwner(ctx context.Context, arg ListResourcesByParentAndOwnerParams) ([]Resource, error) {
rows, err := q.db.QueryContext(ctx, listResourcesByParentAndOwner,
arg.ParentResourceID,
arg.OwnerID,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Resource
for rows.Next() {
var i Resource
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listResourcesByParentID = `-- name: ListResourcesByParentID :many
SELECT id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at FROM resources
WHERE parent_resource_id = $1
ORDER BY is_folder DESC, created_at DESC
`
func (q *Queries) ListResourcesByParentID(ctx context.Context, parentResourceID uuid.NullUUID) ([]Resource, error) {
rows, err := q.db.QueryContext(ctx, listResourcesByParentID, parentResourceID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Resource
for rows.Next() {
var i Resource
if err := rows.Scan(
&i.ID,
&i.Name,
&i.MimeType,
&i.Size,
&i.Checksum,
&i.OcrText,
&i.IsFolder,
&i.ParentResourceID,
&i.OwnerID,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const moveResources = `-- name: MoveResources :exec
UPDATE resources
SET parent_resource_id = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = ANY($2::uuid[])
`
type MoveResourcesParams struct {
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
Column2 []uuid.UUID `json:"column_2"`
}
func (q *Queries) MoveResources(ctx context.Context, arg MoveResourcesParams) error {
_, err := q.db.ExecContext(ctx, moveResources, arg.ParentResourceID, pq.Array(arg.Column2))
return err
}
const updateResource = `-- name: UpdateResource :exec
UPDATE resources
SET name = $1, mime_type = $2, ocr_text = $3, updated_at = CURRENT_TIMESTAMP
WHERE id = $4
`
type UpdateResourceParams struct {
Name string `json:"name"`
MimeType string `json:"mime_type"`
OcrText string `json:"ocr_text"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateResource(ctx context.Context, arg UpdateResourceParams) error {
_, err := q.db.ExecContext(ctx, updateResource,
arg.Name,
arg.MimeType,
arg.OcrText,
arg.ID,
)
return err
}
@@ -1,144 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: retention_policies.sql
package db
import (
"context"
"encoding/json"
"github.com/google/uuid"
)
const createRetentionPolicy = `-- name: CreateRetentionPolicy :one
INSERT INTO retention_policies (user_id, storage_location_id, rule_type, rule_value)
VALUES ($1, $2, $3, $4)
RETURNING id, user_id, storage_location_id, rule_type, rule_value, created_at
`
type CreateRetentionPolicyParams struct {
UserID uuid.UUID `json:"user_id"`
StorageLocationID uuid.UUID `json:"storage_location_id"`
RuleType string `json:"rule_type"`
RuleValue json.RawMessage `json:"rule_value"`
}
func (q *Queries) CreateRetentionPolicy(ctx context.Context, arg CreateRetentionPolicyParams) (RetentionPolicy, error) {
row := q.db.QueryRowContext(ctx, createRetentionPolicy,
arg.UserID,
arg.StorageLocationID,
arg.RuleType,
arg.RuleValue,
)
var i RetentionPolicy
err := row.Scan(
&i.ID,
&i.UserID,
&i.StorageLocationID,
&i.RuleType,
&i.RuleValue,
&i.CreatedAt,
)
return i, err
}
const deleteRetentionPolicy = `-- name: DeleteRetentionPolicy :exec
DELETE FROM retention_policies
WHERE id = $1
`
func (q *Queries) DeleteRetentionPolicy(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteRetentionPolicy, id)
return err
}
const getRetentionPolicy = `-- name: GetRetentionPolicy :one
SELECT id, user_id, storage_location_id, rule_type, rule_value, created_at FROM retention_policies
WHERE id = $1
`
func (q *Queries) GetRetentionPolicy(ctx context.Context, id uuid.UUID) (RetentionPolicy, error) {
row := q.db.QueryRowContext(ctx, getRetentionPolicy, id)
var i RetentionPolicy
err := row.Scan(
&i.ID,
&i.UserID,
&i.StorageLocationID,
&i.RuleType,
&i.RuleValue,
&i.CreatedAt,
)
return i, err
}
const listRetentionPoliciesByLocation = `-- name: ListRetentionPoliciesByLocation :many
SELECT id, user_id, storage_location_id, rule_type, rule_value, created_at FROM retention_policies
WHERE storage_location_id = $1
`
func (q *Queries) ListRetentionPoliciesByLocation(ctx context.Context, storageLocationID uuid.UUID) ([]RetentionPolicy, error) {
rows, err := q.db.QueryContext(ctx, listRetentionPoliciesByLocation, storageLocationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RetentionPolicy
for rows.Next() {
var i RetentionPolicy
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.StorageLocationID,
&i.RuleType,
&i.RuleValue,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRetentionPoliciesByUser = `-- name: ListRetentionPoliciesByUser :many
SELECT id, user_id, storage_location_id, rule_type, rule_value, created_at FROM retention_policies
WHERE user_id = $1
`
func (q *Queries) ListRetentionPoliciesByUser(ctx context.Context, userID uuid.UUID) ([]RetentionPolicy, error) {
rows, err := q.db.QueryContext(ctx, listRetentionPoliciesByUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RetentionPolicy
for rows.Next() {
var i RetentionPolicy
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.StorageLocationID,
&i.RuleType,
&i.RuleValue,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
@@ -1,134 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: storage_locations.sql
package db
import (
"context"
"github.com/google/uuid"
)
const createStorageLocation = `-- name: CreateStorageLocation :one
INSERT INTO storage_locations (user_id, device_name, role)
VALUES ($1, $2, $3)
RETURNING id, user_id, device_name, role, created_at, last_seen_at
`
type CreateStorageLocationParams struct {
UserID uuid.UUID `json:"user_id"`
DeviceName string `json:"device_name"`
Role string `json:"role"`
}
func (q *Queries) CreateStorageLocation(ctx context.Context, arg CreateStorageLocationParams) (StorageLocation, error) {
row := q.db.QueryRowContext(ctx, createStorageLocation, arg.UserID, arg.DeviceName, arg.Role)
var i StorageLocation
err := row.Scan(
&i.ID,
&i.UserID,
&i.DeviceName,
&i.Role,
&i.CreatedAt,
&i.LastSeenAt,
)
return i, err
}
const deleteStorageLocation = `-- name: DeleteStorageLocation :exec
DELETE FROM storage_locations
WHERE id = $1
`
func (q *Queries) DeleteStorageLocation(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteStorageLocation, id)
return err
}
const getServerStorageLocation = `-- name: GetServerStorageLocation :one
SELECT id, user_id, device_name, role, created_at, last_seen_at FROM storage_locations
WHERE user_id = $1 AND role = 'server'
LIMIT 1
`
func (q *Queries) GetServerStorageLocation(ctx context.Context, userID uuid.UUID) (StorageLocation, error) {
row := q.db.QueryRowContext(ctx, getServerStorageLocation, userID)
var i StorageLocation
err := row.Scan(
&i.ID,
&i.UserID,
&i.DeviceName,
&i.Role,
&i.CreatedAt,
&i.LastSeenAt,
)
return i, err
}
const getStorageLocation = `-- name: GetStorageLocation :one
SELECT id, user_id, device_name, role, created_at, last_seen_at FROM storage_locations
WHERE id = $1
`
func (q *Queries) GetStorageLocation(ctx context.Context, id uuid.UUID) (StorageLocation, error) {
row := q.db.QueryRowContext(ctx, getStorageLocation, id)
var i StorageLocation
err := row.Scan(
&i.ID,
&i.UserID,
&i.DeviceName,
&i.Role,
&i.CreatedAt,
&i.LastSeenAt,
)
return i, err
}
const listStorageLocationsByUser = `-- name: ListStorageLocationsByUser :many
SELECT id, user_id, device_name, role, created_at, last_seen_at FROM storage_locations
WHERE user_id = $1
ORDER BY created_at ASC
`
func (q *Queries) ListStorageLocationsByUser(ctx context.Context, userID uuid.UUID) ([]StorageLocation, error) {
rows, err := q.db.QueryContext(ctx, listStorageLocationsByUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []StorageLocation
for rows.Next() {
var i StorageLocation
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.DeviceName,
&i.Role,
&i.CreatedAt,
&i.LastSeenAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateStorageLocationLastSeen = `-- name: UpdateStorageLocationLastSeen :exec
UPDATE storage_locations
SET last_seen_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) UpdateStorageLocationLastSeen(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, updateStorageLocationLastSeen, id)
return err
}
-172
View File
@@ -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
}
-220
View File
@@ -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
}
-65
View File
@@ -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,
})
}
-34
View File
@@ -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
})
}
-49
View File
@@ -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),
}
}
-44
View File
@@ -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,
})
}
-22
View File
@@ -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")
}
-523
View File
@@ -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),
})
}
-64
View File
@@ -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)
}
-89
View File
@@ -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 != "",
})
}
-56
View File
@@ -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",
})
}
-42
View File
@@ -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"`
}
-10
View File
@@ -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"`
}
-160
View File
@@ -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
}
-27
View File
@@ -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"`
}
-10
View File
@@ -1,10 +0,0 @@
package service
import (
"crypto/sha256"
)
func CreateSHA256Hash(data []byte) []byte {
h := sha256.Sum256(data)
return h[:]
}
-262
View File
@@ -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
}
-54
View File
@@ -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:
}
}
}
-176
View File
@@ -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)
}
-55
View File
@@ -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)
}
-89
View File
@@ -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)
}
-539
View File
@@ -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
}
-74
View File
@@ -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,
})
}
-67
View File
@@ -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))
}
+38
View File
@@ -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)
}
-24
View File
@@ -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"]
-115
View File
@@ -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)},
)
-38
View File
@@ -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,
},
})
}
-12
View File
@@ -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
View File
+11
View File
@@ -0,0 +1,11 @@
package main
type User struct {
Username string
}
func NewUser(username string) (error, User) {
return nil, User{
Username: username,
}
}