V3 version with backward comp
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
# VaultDrop Backend — Spécification V1 (État Existant)
|
||||
|
||||
## 1. Vue d'ensemble
|
||||
|
||||
Application de gestion de documents : upload, scan OCR, tagging, recherche, organisation en dossiers. Backend Go avec API REST, base PostgreSQL, microservice OCR Python séparé.
|
||||
|
||||
```
|
||||
┌──────────────────┐ ┌──────────────────────┐
|
||||
│ Mobile App │ HTTPS │ Go Backend (Gin) │
|
||||
│ (React Native) │────────>│ REST API │
|
||||
│ │ │ - Upload / Tags │
|
||||
│ │ │ - Auth (PASETO) │
|
||||
│ │ │ - Thumbnails │
|
||||
└──────────────────┘ │ - Conversion PDF │
|
||||
└────────┬─────────────┘
|
||||
│ HTTP
|
||||
┌────────▼─────────────┐
|
||||
│ OCR Server (Python) │
|
||||
│ PaddleOCR + FastAPI │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Choix Techniques
|
||||
|
||||
| Composant | Technologie |
|
||||
|---|---|
|
||||
| Framework HTTP | Gin v1.12 |
|
||||
| Base de données | PostgreSQL (lib/pq) |
|
||||
| ORM/Query | sqlc v1.31 (généré) |
|
||||
| Migrations | golang-migrate v4 |
|
||||
| Auth | PASETO v4 (go-paseto) + Argon2id |
|
||||
| OCR | PaddleOCR (microservice Python séparé) |
|
||||
| Conversion PDF | LibreOffice + pdftoppm (appels système) |
|
||||
| Checksum | SHA-256 |
|
||||
| Stockage fichiers | Disque local (`./uploads/`) |
|
||||
| URLs signées | HMAC-SHA256 avec expiry |
|
||||
|
||||
## 3. Structure du Code
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/server/main.go # Point d'entrée
|
||||
├── internal/
|
||||
│ ├── auth/ # Auth (3 fichiers)
|
||||
│ │ ├── service.go # Register, Login, Refresh, Logout, ValidateAccessToken
|
||||
│ │ ├── handler.go # HTTP handlers auth
|
||||
│ │ └── middleware.go # RequireAuth() middleware
|
||||
│ ├── config/config.go # Config struct (11 vars d'env)
|
||||
│ ├── db/
|
||||
│ │ ├── connect.go # Connexion PostgreSQL
|
||||
│ │ ├── migrate.go # golang-migrate
|
||||
│ │ ├── db.go # sqlc Queries interface
|
||||
│ │ ├── models.go # sqlc models (généré)
|
||||
│ │ ├── files.sql.go # sqlc queries fichiers (généré)
|
||||
│ │ ├── tags.sql.go # sqlc queries tags (généré)
|
||||
│ │ ├── thumbnails.sql.go # sqlc queries thumbnails (généré)
|
||||
│ │ ├── health.sql.go # sqlc health check
|
||||
│ │ ├── auth.sql.go # sqlc queries auth (généré)
|
||||
│ │ ├── queries/ # SQL sources (5 fichiers .sql)
|
||||
│ │ └── migrations/ # 11 migrations (001→011)
|
||||
│ ├── handler/
|
||||
│ │ ├── handler.go # Handler struct + constructor
|
||||
│ │ ├── router.go # SetupRoutes()
|
||||
│ │ ├── files.go # 14 handlers fichiers/thumbnails
|
||||
│ │ ├── ocr.go # OCR stubs (501/404)
|
||||
│ │ └── health.go # Health check
|
||||
│ ├── model/
|
||||
│ │ ├── file.go # File, Tag, UploadResult structs
|
||||
│ │ ├── ocrjob.go # OcrJob struct
|
||||
│ │ ├── tag.go # (vide)
|
||||
│ │ └── thumbnail.go # Thumbnail struct
|
||||
│ ├── ocr/
|
||||
│ │ ├── client.go # Client HTTP PaddleOCR + détection doc type
|
||||
│ │ └── types.go # OCR request/response types
|
||||
│ └── service/
|
||||
│ ├── file.go # FileService: upload, list, get, delete, tags, folders, thumbnails, dedup
|
||||
│ ├── ocr.go # OCRService: worker async, enqueue, process
|
||||
│ ├── conversion.go # ConversionService: PDF→images, thumbnails multi-résolution
|
||||
│ ├── url.go # URLService: signed URLs HMAC
|
||||
│ └── checksum.go # SHA-256 hash helpers
|
||||
├── pkg/api/response.go # Helpers: Success, Created, Paginated, Error
|
||||
├── ocr-server/
|
||||
│ ├── server.py # FastAPI + PaddleOCR
|
||||
│ └── Dockerfile
|
||||
├── uploads/ # Stockage fichiers + thumbnails
|
||||
├── sqlc.yaml
|
||||
├── Dockerfile
|
||||
├── go.mod
|
||||
└── .env
|
||||
```
|
||||
|
||||
## 4. Base de Données (Schéma Final)
|
||||
|
||||
### Tables
|
||||
|
||||
**files**
|
||||
|
||||
| Colonne | Type | Contraintes |
|
||||
|---|---|---|
|
||||
| id | TEXT | PK, 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 | FK → files(id) |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
**tags**
|
||||
|
||||
| Colonne | Type | Contraintes |
|
||||
|---|---|---|
|
||||
| id | TEXT | PK, DEFAULT gen_random_uuid() |
|
||||
| parent_tag_id | TEXT | FK → tags(id) |
|
||||
| tag_name | TEXT | NOT NULL |
|
||||
| tag_type | TEXT | NOT NULL DEFAULT 'none' |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
**file_tags**
|
||||
|
||||
| Colonne | Type | Contraintes |
|
||||
|---|---|---|
|
||||
| id | TEXT | PK, DEFAULT gen_random_uuid() |
|
||||
| tag_id | TEXT | FK → tags(id) |
|
||||
| file_id | TEXT | FK → files(id) |
|
||||
|
||||
**users**
|
||||
|
||||
| Colonne | Type | Contraintes |
|
||||
|---|---|---|
|
||||
| id | TEXT | PK, DEFAULT gen_random_uuid() |
|
||||
| username | TEXT | NOT NULL, UNIQUE |
|
||||
| password_hash | TEXT | NOT NULL |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
**refresh_tokens**
|
||||
|
||||
| Colonne | Type | Contraintes |
|
||||
|---|---|---|
|
||||
| id | TEXT | PK, DEFAULT gen_random_uuid() |
|
||||
| user_id | TEXT | FK → users(id), ON DELETE CASCADE |
|
||||
| token_hash | TEXT | NOT NULL |
|
||||
| expires_at | TIMESTAMP | NOT NULL |
|
||||
| revoked | BOOLEAN | DEFAULT FALSE |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
**thumbnails**
|
||||
|
||||
| Colonne | Type | Contraintes |
|
||||
|---|---|---|
|
||||
| id | TEXT | PK, DEFAULT gen_random_uuid() |
|
||||
| file_id | TEXT | FK → 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 | DEFAULT 'image/jpeg' |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
### Index
|
||||
|
||||
- `idx_refresh_tokens_user_id` ON refresh_tokens(user_id)
|
||||
- `idx_refresh_tokens_token_hash` ON refresh_tokens(token_hash)
|
||||
- `idx_thumbnails_file_id` ON thumbnails(file_id)
|
||||
- `idx_thumbnails_unique` UNIQUE ON thumbnails(file_id, page_number, resolution_label)
|
||||
|
||||
## 5. API Endpoints
|
||||
|
||||
### Public
|
||||
|
||||
| Méthode | Path | Handler | Description |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/v1/health` | HealthHandler.Check | Retourne `{status: "healthy"}` |
|
||||
| POST | `/api/v1/auth/register` | AuthHandler.Register | Créer un compte (username + password) |
|
||||
| POST | `/api/v1/auth/login` | AuthHandler.Login | Login → access_token + refresh_token |
|
||||
| POST | `/api/v1/auth/refresh` | AuthHandler.Refresh | Renouveler les tokens |
|
||||
| POST | `/api/v1/auth/logout` | AuthHandler.Logout | Révoquer le refresh token |
|
||||
| GET | `/api/v1/files/download/:id` | FileHandler.Download | Télécharger un fichier (URL signée) |
|
||||
| GET | `/api/v1/thumbnails/:id` | FileHandler.ServeThumbnail | Télécharger une thumbnail (URL signée) |
|
||||
|
||||
### Protégées (Bearer token)
|
||||
|
||||
| Méthode | Path | Handler | Description |
|
||||
|---|---|---|---|
|
||||
| GET | `/api/v1/files` | FileHandler.List | Lister tous les fichiers (racine) |
|
||||
| POST | `/api/v1/files/upload` | FileHandler.Upload | Upload multi-fichiers (multipart) |
|
||||
| POST | `/api/v1/files/move` | FileHandler.MoveFiles | Déplacer des fichiers vers un dossier |
|
||||
| POST | `/api/v1/files/folders` | FileHandler.CreateFolder | Créer un dossier |
|
||||
| GET | `/api/v1/files/folders` | FileHandler.ListFolders | Lister les dossiers |
|
||||
| GET | `/api/v1/files/folders/:id/files` | FileHandler.ListFilesByParent | Lister les fichiers d'un dossier |
|
||||
| DELETE | `/api/v1/files/:id` | FileHandler.Delete | Supprimer un fichier (+ thumbnails + fichier disque) |
|
||||
| GET | `/api/v1/files/:id` | FileHandler.Get | Détail d'un fichier (+ tags + thumbnails) |
|
||||
| POST | `/api/v1/files/:id/tags` | FileHandler.AddTags | Ajouter des tags à un fichier |
|
||||
| GET | `/api/v1/files/:id/tags` | FileHandler.GetTags | Récupérer les tags d'un fichier |
|
||||
| POST | `/api/v1/files/:id/thumbnails` | FileHandler.GetThumbnails | Lister les thumbnails d'un fichier |
|
||||
| POST | `/api/v1/files/dedup-check` | FileHandler.CheckDuplicates | Vérifier les doublons (nom + taille) |
|
||||
| POST | `/api/v1/ocr/jobs` | OCRHandler.CreateJob | **STUB** — retourne 501 |
|
||||
| GET | `/api/v1/ocr/jobs/:id` | OCRHandler.GetJobStatus | **STUB** — retourne 404 |
|
||||
|
||||
### Format de réponse
|
||||
|
||||
Succès : `{ "data": ... }` ou `{ "data": ..., "meta": { "page": N, "total": N } }`
|
||||
Erreur : `{ "error": { "code": "...", "message": "..." } }`
|
||||
|
||||
## 6. Services Métier
|
||||
|
||||
### FileService (service/file.go — 349 lignes)
|
||||
|
||||
- `Upload(file)` — Upload multipart, checksum SHA-256, détection doublons par checksum, stockage disque
|
||||
- `List()` — Tous les fichiers racine (parent_file_id IS NULL), avec tags
|
||||
- `Get(id)` — Fichier par ID avec tags
|
||||
- `Delete(id)` — Suppression DB (handler gère suppression disque + thumbnails)
|
||||
- `GetStoragePath(id)` — Chemin disque du fichier
|
||||
- `UpdateOCRText(id, text)` — Mise à jour du champ ocr_text
|
||||
- `AddTags(fileID, tags, tagType)` — Création auto des tags + liaison
|
||||
- `GetTagsByFileID(fileID)` — Tags d'un fichier
|
||||
- `MoveFiles(fileIDs, parentFileID)` — Déplacer vers un dossier (ou racine si nil)
|
||||
- `CreateFolder(name)` — Créer un dossier
|
||||
- `ListFolders()` — Lister tous les dossiers
|
||||
- `ListFilesByParentID(parentID)` — Fichiers d'un dossier
|
||||
- `GetThumbnailsByFileID(fileID)` — Thumbnails d'un fichier
|
||||
- `GetThumbnailStoragePath(id)` — Chemin disque thumbnail
|
||||
- `GetBestThumbnail(fileID, label)` — Meilleure thumbnail (page 1, label préféré ou fallback)
|
||||
- `FindDuplicatesByNameSize(name, size)` — Recherche doublons par nom+taille
|
||||
|
||||
### OCRService (service/ocr.go — 95 lignes)
|
||||
|
||||
- `Start()` — Lance le worker goroutine
|
||||
- `Stop()` — Ferme le channel jobs
|
||||
- `Enqueue(fileID, filePath)` — Ajoute un job OCR (channel buffer 100)
|
||||
- `process(job)` — Lecture fichier → Client OCR → Flatten text → UpdateOCRText
|
||||
- `RecognizeFromBytes(data)` — OCR direct (pas utilisé par les handlers)
|
||||
- `HealthCheck()` — Vérifie l'OCR server
|
||||
|
||||
### ConversionService (service/conversion.go — 253 lignes)
|
||||
|
||||
- `Start()` / `Stop()` — Worker goroutine
|
||||
- `Enqueue(fileID, filePath, mimeType)` — Conversion async (channel buffer 100)
|
||||
- `process(job)` :
|
||||
1. Si document Office → conversion vers PDF via LibreOffice headless
|
||||
2. Si PDF → direct
|
||||
3. PDF → images via pdftoppm à 2 résolutions : "thumbnail" (21 DPI) et "full" (200 DPI)
|
||||
4. Création des enregistrements thumbnails en DB
|
||||
- Supporte : PDF, .docx, .xlsx, .pptx, formats OpenDocument
|
||||
|
||||
### URLService (service/url.go — 67 lignes)
|
||||
|
||||
- `GenerateDownloadURL(fileUUID)` — URL signée HMAC avec expiry (défaut 60 min)
|
||||
- `GenerateThumbnailURL(thumbUUID)` — Idem pour thumbnails
|
||||
- `Validate(fileID, sig, expires)` — Vérifie signature HMAC + expiry
|
||||
|
||||
### Auth (auth/ — 250+135+41 lignes)
|
||||
|
||||
- **Register** : validation username (3-30 chars) + password (8+ chars), Argon2id hash, PASETO v4 tokens
|
||||
- **Login** : vérification credentials, émission token pair
|
||||
- **Refresh** : révoque l'ancien refresh token, émet un nouveau pair
|
||||
- **Logout** : révoque le refresh token
|
||||
- **Middleware** : Bearer token extraction → ValidateAccessToken → set userID in context
|
||||
- Access token TTL : 30 min | Refresh token TTL : 7 jours
|
||||
|
||||
## 7. OCR Server (Python)
|
||||
|
||||
**ocr-server/server.py** — FastAPI + PaddleOCR
|
||||
|
||||
- `GET /health` — health check
|
||||
- `POST /ocr` — OCR via JSON base64
|
||||
- `POST /ocr/upload` — OCR via upload fichier
|
||||
- Supporte images (JPEG/PNG) et PDF (conversion vers images via pypdfium2)
|
||||
- Langue par défaut : français (`OCR_LANG=fr`)
|
||||
- Timeout client Go : 120 secondes
|
||||
|
||||
## 8. Détection de Doublons
|
||||
|
||||
Deux mécanismes :
|
||||
|
||||
1. **Par checksum (SHA-256)** : lors de l'upload, si un fichier avec le même checksum existe déjà, le fichier uploadé est supprimé et l'ID existant retourné
|
||||
2. **Par nom+taille** : endpoint `POST /files/dedup-check` pour vérifier avant upload
|
||||
|
||||
## 9. Variables d'Environnement
|
||||
|
||||
| Variable | Défaut | Description |
|
||||
|---|---|---|
|
||||
| PORT | `8080` | Port du serveur |
|
||||
| DATABASE_URL | `postgres://localhost:5432/vaultdrop?sslmode=disable` | URL PostgreSQL |
|
||||
| OCR_ENDPOINT | `http://localhost:9090` | URL du microservice OCR |
|
||||
| UPLOAD_DIR | `./uploads` | Répertoire de stockage |
|
||||
| HMAC_SECRET | `thisismyrandomstring` | Secret pour URLs signées |
|
||||
| SERVER_HOST | `http://192.168.1.17:8080` | Host public du serveur |
|
||||
| PASETO_KEY | (clé hex 64 car.) | Clé symétrique PASETO v4 |
|
||||
| LIBREOFFICE_PATH | `/usr/bin/libreoffice` | Chemin LibreOffice |
|
||||
| PDFTOPPM_PATH | `/usr/bin/pdftoppm` | Chemin pdftoppm |
|
||||
| THUMBNAIL_DIR | `./uploads/thumbnails` | Répertoire thumbnails |
|
||||
| URL_EXPIRY_MINUTES | `60` | Durée de vie des URLs signées |
|
||||
|
||||
## 10. Docker
|
||||
|
||||
**Backend** : Multi-stage build (golang:1.24-alpine → alpine:3.21 avec libreoffice-core + poppler-utils)
|
||||
|
||||
**OCR Server** : Dockerfile séparé dans `ocr-server/`
|
||||
|
||||
## 11. Ce qui manque / Stubs
|
||||
|
||||
- `OCRHandler.CreateJob` → retourne 501 NOT_IMPLEMENTED
|
||||
- `OCRHandler.GetJobStatus` → retourne 404 JOB_NOT_FOUND
|
||||
- Pas de recherche full-text (pas de FTS5/PostgreSQL tsvector)
|
||||
- Pas de pagination côté DB (tout est chargé en mémoire)
|
||||
- Pas de gestion d'erreurs robuste dans le worker OCR (logs seulement)
|
||||
- Les fichiers n'ont pas de lien avec les utilisateurs (pas de user_id sur files)
|
||||
- Le health check ne vérifie pas la DB ni l'OCR server
|
||||
@@ -30,8 +30,8 @@ func main() {
|
||||
|
||||
queries := db.New(database)
|
||||
|
||||
fileSvc := service.NewFileService(queries, cfg)
|
||||
ocrSvc := service.NewOCRService(cfg, fileSvc)
|
||||
resourceSvc := service.NewResourceService(queries, cfg)
|
||||
ocrSvc := service.NewOCRService(cfg, resourceSvc)
|
||||
conversionSvc := service.NewConversionService(queries, cfg)
|
||||
urlSvc := service.NewURLService(cfg.HMACSecret, cfg.ServerHost, cfg.URLExpiryMinutes)
|
||||
authSvc, err := auth.NewAuthService(queries, cfg)
|
||||
@@ -39,13 +39,17 @@ func main() {
|
||||
log.Fatalf("Failed to create auth service: %v", err)
|
||||
}
|
||||
|
||||
rebacSvc := service.NewRebacService(queries)
|
||||
placementSvc := service.NewPlacementService(queries)
|
||||
syncSvc := service.NewSyncService(queries)
|
||||
|
||||
ocrSvc.Start()
|
||||
defer ocrSvc.Stop()
|
||||
|
||||
conversionSvc.Start()
|
||||
defer conversionSvc.Stop()
|
||||
|
||||
h := handler.New(fileSvc, ocrSvc, urlSvc, auth.NewAuthHandler(authSvc), conversionSvc)
|
||||
h := handler.New(resourceSvc, ocrSvc, urlSvc, auth.NewAuthHandler(authSvc), conversionSvc, rebacSvc, placementSvc, syncSvc)
|
||||
|
||||
r := gin.Default()
|
||||
handler.SetupRoutes(r, h, authSvc)
|
||||
|
||||
@@ -39,7 +39,7 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (username, password_hash)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, username, password_hash, created_at, updated_at
|
||||
RETURNING id, username, password_hash, created_at, updated_at, parent_user_id
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
@@ -56,6 +56,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
&i.PasswordHash,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentUserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -80,7 +81,7 @@ func (q *Queries) GetRefreshToken(ctx context.Context, tokenHash string) (Refres
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, username, password_hash, created_at, updated_at FROM users WHERE id = $1
|
||||
SELECT id, username, password_hash, created_at, updated_at, parent_user_id FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) {
|
||||
@@ -92,12 +93,13 @@ func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) {
|
||||
&i.PasswordHash,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentUserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = $1
|
||||
SELECT id, username, password_hash, created_at, updated_at, parent_user_id FROM users WHERE username = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
||||
@@ -109,6 +111,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
|
||||
&i.PasswordHash,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentUserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: files.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
const createFile = `-- name: CreateFile :one
|
||||
INSERT INTO files (name, mime_type, size, storage_key, checksum, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder
|
||||
`
|
||||
|
||||
type CreateFileParams struct {
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int64 `json:"size"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
|
||||
row := q.db.QueryRowContext(ctx, createFile,
|
||||
arg.Name,
|
||||
arg.MimeType,
|
||||
arg.Size,
|
||||
arg.StorageKey,
|
||||
arg.Checksum,
|
||||
)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createFolder = `-- name: CreateFolder :one
|
||||
INSERT INTO files (name, is_folder, created_at, updated_at)
|
||||
VALUES ($1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder
|
||||
`
|
||||
|
||||
func (q *Queries) CreateFolder(ctx context.Context, name string) (File, error) {
|
||||
row := q.db.QueryRowContext(ctx, createFolder, name)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteFile = `-- name: DeleteFile :exec
|
||||
DELETE FROM files
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteFile(ctx context.Context, id string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteFile, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const findDuplicateByChecksum = `-- name: FindDuplicateByChecksum :one
|
||||
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder FROM files
|
||||
WHERE checksum = $1 AND is_folder = false
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) FindDuplicateByChecksum(ctx context.Context, checksum string) (File, error) {
|
||||
row := q.db.QueryRowContext(ctx, findDuplicateByChecksum, checksum)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const findDuplicatesByNameSize = `-- name: FindDuplicatesByNameSize :many
|
||||
SELECT id, name, mime_type, size, checksum, created_at FROM files
|
||||
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 string `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 getFile = `-- name: GetFile :one
|
||||
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder FROM files
|
||||
WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetFile(ctx context.Context, id string) (File, error) {
|
||||
row := q.db.QueryRowContext(ctx, getFile, id)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listFiles = `-- name: ListFiles :many
|
||||
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder FROM files
|
||||
WHERE parent_file_id IS NULL
|
||||
ORDER BY is_folder DESC, created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFiles(ctx context.Context) ([]File, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []File
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
); 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 listFilesByID = `-- name: ListFilesByID :many
|
||||
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder FROM files
|
||||
WHERE id = ANY($1::text[])
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFilesByID(ctx context.Context, dollar_1 []string) ([]File, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFilesByID, pq.Array(dollar_1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []File
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
); 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 listFilesByParentID = `-- name: ListFilesByParentID :many
|
||||
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder FROM files
|
||||
WHERE parent_file_id = $1
|
||||
ORDER BY is_folder DESC, created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFilesByParentID(ctx context.Context, parentFileID sql.NullString) ([]File, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFilesByParentID, parentFileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []File
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
); 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 listFolders = `-- name: ListFolders :many
|
||||
SELECT id, name, mime_type, size, storage_key, checksum, ocr_text, created_at, updated_at, parent_file_id, is_folder FROM files
|
||||
WHERE is_folder = true
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFolders(ctx context.Context) ([]File, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listFolders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []File
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
); 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 moveFiles = `-- name: MoveFiles :exec
|
||||
UPDATE files
|
||||
SET parent_file_id = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ANY($2::text[])
|
||||
`
|
||||
|
||||
type MoveFilesParams struct {
|
||||
ParentFileID sql.NullString `json:"parent_file_id"`
|
||||
Column2 []string `json:"column_2"`
|
||||
}
|
||||
|
||||
func (q *Queries) MoveFiles(ctx context.Context, arg MoveFilesParams) error {
|
||||
_, err := q.db.ExecContext(ctx, moveFiles, arg.ParentFileID, pq.Array(arg.Column2))
|
||||
return err
|
||||
}
|
||||
|
||||
const updateFile = `-- name: UpdateFile :exec
|
||||
UPDATE files
|
||||
SET name = $1, mime_type = $2, ocr_text = $3, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $4
|
||||
`
|
||||
|
||||
type UpdateFileParams struct {
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mime_type"`
|
||||
OcrText string `json:"ocr_text"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateFile,
|
||||
arg.Name,
|
||||
arg.MimeType,
|
||||
arg.OcrText,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
-- VaultDrop 012 down: drop V3 new tables
|
||||
DROP FUNCTION IF EXISTS resolve_effective_role;
|
||||
DROP TABLE IF EXISTS rebac_relations;
|
||||
DROP TABLE IF EXISTS sync_queue;
|
||||
DROP TABLE IF EXISTS retention_policies;
|
||||
DROP TABLE IF EXISTS resource_placements;
|
||||
DROP TABLE IF EXISTS storage_locations;
|
||||
DROP TABLE IF EXISTS resource_variants;
|
||||
DROP TABLE IF EXISTS resources;
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS parent_user_id;
|
||||
@@ -0,0 +1,145 @@
|
||||
-- VaultDrop 012: V3 new tables - resources, storage_locations, resource_placements, etc.
|
||||
|
||||
-- Add parent_user_id to users for groups/organizations
|
||||
ALTER TABLE users ADD COLUMN parent_user_id UUID REFERENCES users(id);
|
||||
|
||||
-- Resources (replaces files)
|
||||
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);
|
||||
|
||||
-- Resource variants (replaces thumbnails)
|
||||
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);
|
||||
|
||||
-- Storage locations (devices, server, backup)
|
||||
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);
|
||||
|
||||
-- Resource placements (pivot resource × storage_location)
|
||||
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);
|
||||
|
||||
-- Retention policies
|
||||
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);
|
||||
|
||||
-- Sync queue
|
||||
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 relations
|
||||
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);
|
||||
|
||||
-- Function to resolve effective role with recursive parent/group traversal
|
||||
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;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- VaultDrop 013 down: revert data migration
|
||||
-- Re-add tag_type to tags
|
||||
ALTER TABLE tags ADD COLUMN IF NOT EXISTS tag_type TEXT NOT NULL DEFAULT 'none';
|
||||
|
||||
DROP TABLE IF EXISTS resource_tags;
|
||||
|
||||
DELETE FROM resource_variants;
|
||||
|
||||
DELETE FROM rebac_relations;
|
||||
|
||||
DELETE FROM resource_placements;
|
||||
|
||||
DELETE FROM storage_locations;
|
||||
|
||||
DELETE FROM resources;
|
||||
@@ -0,0 +1,100 @@
|
||||
-- VaultDrop 013: Migrate data from V1 to V3
|
||||
-- Assigns all existing files to user 'pixel'
|
||||
|
||||
-- 1. Ensure user 'pixel' exists (create if not)
|
||||
INSERT INTO users (username, password_hash)
|
||||
SELECT 'pixel', '$argon2id$v=19$m=65536,t=1,p=4$placeholder$placeholder'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM users WHERE username = 'pixel');
|
||||
|
||||
-- 2. Migrate files -> resources
|
||||
INSERT INTO resources (id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at)
|
||||
SELECT
|
||||
f.id::uuid,
|
||||
f.name,
|
||||
f.mime_type,
|
||||
f.size,
|
||||
f.checksum,
|
||||
f.ocr_text,
|
||||
f.is_folder,
|
||||
f.parent_file_id::uuid,
|
||||
(SELECT id FROM users WHERE username = 'pixel'),
|
||||
f.created_at,
|
||||
f.updated_at
|
||||
FROM files f
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 3. Create server storage_location for user 'pixel'
|
||||
INSERT INTO storage_locations (id, user_id, device_name, role)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
(SELECT id FROM users WHERE username = 'pixel'),
|
||||
'VaultDrop Server',
|
||||
'server'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM storage_locations
|
||||
WHERE user_id = (SELECT id FROM users WHERE username = 'pixel')
|
||||
AND role = 'server'
|
||||
);
|
||||
|
||||
-- 4. Create resource_placements for migrated resources
|
||||
INSERT INTO resource_placements (resource_id, storage_location_id, status, storage_key, synced_at)
|
||||
SELECT
|
||||
r.id,
|
||||
sl.id,
|
||||
'synced',
|
||||
f.storage_key,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM resources r
|
||||
JOIN files f ON f.id::uuid = r.id
|
||||
CROSS JOIN storage_locations sl
|
||||
WHERE sl.user_id = (SELECT id FROM users WHERE username = 'pixel')
|
||||
AND sl.role = 'server'
|
||||
ON CONFLICT (resource_id, storage_location_id) DO NOTHING;
|
||||
|
||||
-- 5. Create rebac_relations (owner) for all migrated resources
|
||||
INSERT INTO rebac_relations (resource_id, subject_user_id, role, granted_by)
|
||||
SELECT
|
||||
r.id,
|
||||
u.id,
|
||||
'owner',
|
||||
u.id
|
||||
FROM resources r
|
||||
CROSS JOIN (SELECT id FROM users WHERE username = 'pixel') u
|
||||
ON CONFLICT (resource_id, subject_user_id) DO NOTHING;
|
||||
|
||||
-- 6. Migrate thumbnails -> resource_variants
|
||||
INSERT INTO resource_variants (id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at)
|
||||
SELECT
|
||||
t.id::uuid,
|
||||
t.file_id::uuid,
|
||||
CASE
|
||||
WHEN t.resolution_label = 'thumbnail' THEN 'thumbnail_small'
|
||||
WHEN t.resolution_label = 'full' THEN 'thumbnail_full'
|
||||
ELSE t.resolution_label
|
||||
END,
|
||||
t.page_number,
|
||||
t.width,
|
||||
t.height,
|
||||
t.mime_type,
|
||||
'server',
|
||||
t.storage_key,
|
||||
t.created_at
|
||||
FROM thumbnails t
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 7. Create resource_tags from file_tags (keep old file_tags for now)
|
||||
CREATE TABLE IF NOT EXISTS resource_tags (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tag_id TEXT,
|
||||
resource_id TEXT
|
||||
);
|
||||
|
||||
INSERT INTO resource_tags (id, tag_id, resource_id)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
ft.tag_id,
|
||||
ft.file_id
|
||||
FROM file_tags ft;
|
||||
|
||||
-- 8. Remove tag_type column from tags
|
||||
ALTER TABLE tags DROP COLUMN IF EXISTS tag_type;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- VaultDrop 014 down: restore legacy tables
|
||||
|
||||
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 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 file_tags (
|
||||
id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
|
||||
tag_id TEXT,
|
||||
file_id TEXT
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- VaultDrop 014: Drop legacy V1 tables
|
||||
|
||||
-- Tags now use resource_tags instead of file_tags
|
||||
DROP TABLE IF EXISTS file_tags;
|
||||
|
||||
-- Thumbnails migrated to resource_variants
|
||||
DROP TABLE IF EXISTS thumbnails;
|
||||
|
||||
-- Files migrated to resources
|
||||
DROP TABLE IF EXISTS files;
|
||||
@@ -6,27 +6,19 @@ package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int64 `json:"size"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
Checksum string `json:"checksum"`
|
||||
OcrText string `json:"ocr_text"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ParentFileID sql.NullString `json:"parent_file_id"`
|
||||
IsFolder bool `json:"is_folder"`
|
||||
}
|
||||
|
||||
type FileTag struct {
|
||||
ID string `json:"id"`
|
||||
TagID sql.NullString `json:"tag_id"`
|
||||
FileID sql.NullString `json:"file_id"`
|
||||
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 {
|
||||
@@ -38,31 +30,91 @@ type RefreshToken struct {
|
||||
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 sql.NullString `json:"tag_id"`
|
||||
ResourceID sql.NullString `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 string `json:"id"`
|
||||
ParentTagID sql.NullString `json:"parent_tag_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TagType string `json:"tag_type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Thumbnail struct {
|
||||
ID string `json:"id"`
|
||||
FileID string `json:"file_id"`
|
||||
PageNumber int32 `json:"page_number"`
|
||||
ResolutionLabel string `json:"resolution_label"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
MimeType string `json:"mime_type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ParentUserID uuid.NullUUID `json:"parent_user_id"`
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
-- name: GetFile :one
|
||||
SELECT * FROM files
|
||||
WHERE id = $1 LIMIT 1;
|
||||
|
||||
-- name: ListFiles :many
|
||||
SELECT * FROM files
|
||||
WHERE parent_file_id IS NULL
|
||||
ORDER BY is_folder DESC, created_at DESC;
|
||||
|
||||
-- name: ListFolders :many
|
||||
SELECT * FROM files
|
||||
WHERE is_folder = true
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListFilesByID :many
|
||||
SELECT * FROM files
|
||||
WHERE id = ANY($1::text[])
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: CreateFile :one
|
||||
INSERT INTO files (name, mime_type, size, storage_key, checksum, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateFolder :one
|
||||
INSERT INTO files (name, is_folder, created_at, updated_at)
|
||||
VALUES ($1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListFilesByParentID :many
|
||||
SELECT * FROM files
|
||||
WHERE parent_file_id = $1
|
||||
ORDER BY is_folder DESC, created_at DESC;
|
||||
|
||||
-- name: MoveFiles :exec
|
||||
UPDATE files
|
||||
SET parent_file_id = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ANY($2::text[]);
|
||||
|
||||
-- name: UpdateFile :exec
|
||||
UPDATE files
|
||||
SET name = $1, mime_type = $2, ocr_text = $3, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $4;
|
||||
|
||||
-- name: DeleteFile :exec
|
||||
DELETE FROM files
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: FindDuplicatesByNameSize :many
|
||||
SELECT id, name, mime_type, size, checksum, created_at FROM files
|
||||
WHERE name = $1 AND size = $2 AND is_folder = false
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: FindDuplicateByChecksum :one
|
||||
SELECT * FROM files
|
||||
WHERE checksum = $1 AND is_folder = false
|
||||
LIMIT 1;
|
||||
@@ -0,0 +1,36 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,67 @@
|
||||
-- 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, created_at, updated_at)
|
||||
VALUES ($1, true, $2, 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: ListResourcesByOwner :many
|
||||
SELECT * FROM resources
|
||||
WHERE owner_id = $1 AND parent_resource_id IS NULL
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: ListResourcesByParentAndOwner :many
|
||||
SELECT * FROM resources
|
||||
WHERE parent_resource_id = $1 AND owner_id = $2
|
||||
ORDER BY is_folder DESC, created_at DESC;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- name: CreateSyncQueueItem :one
|
||||
INSERT INTO sync_queue (resource_id, storage_location_id, operation, status, attempts)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSyncQueueItem :one
|
||||
SELECT * FROM sync_queue
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListPendingSyncItems :many
|
||||
SELECT * FROM sync_queue
|
||||
WHERE status = 'pending'
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: ListPendingSyncItemsByLocation :many
|
||||
SELECT * FROM sync_queue
|
||||
WHERE storage_location_id = $1 AND status = 'pending'
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: UpdateSyncQueueStatus :exec
|
||||
UPDATE sync_queue
|
||||
SET status = $1, attempts = $2, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $3;
|
||||
|
||||
-- name: DeleteSyncQueueItem :exec
|
||||
DELETE FROM sync_queue
|
||||
WHERE id = $1;
|
||||
@@ -1,6 +1,6 @@
|
||||
-- name: CreateTag :one
|
||||
INSERT INTO tags (tag_name, tag_type)
|
||||
VALUES ($1, $2)
|
||||
INSERT INTO tags (tag_name)
|
||||
VALUES ($1)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetTag :one
|
||||
@@ -19,23 +19,23 @@ ORDER BY tag_name ASC;
|
||||
DELETE FROM tags
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: AddTagToFile :exec
|
||||
INSERT INTO file_tags (tag_id, file_id)
|
||||
-- name: AddTagToResource :exec
|
||||
INSERT INTO resource_tags (tag_id, resource_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- name: RemoveTagFromFile :exec
|
||||
DELETE FROM file_tags
|
||||
WHERE tag_id = $1 AND file_id = $2;
|
||||
-- name: RemoveTagFromResource :exec
|
||||
DELETE FROM resource_tags
|
||||
WHERE tag_id = $1 AND resource_id = $2;
|
||||
|
||||
-- name: GetTagsByFileID :many
|
||||
-- name: GetTagsByResourceID :many
|
||||
SELECT t.* FROM tags t
|
||||
JOIN file_tags ft ON t.id = ft.tag_id
|
||||
WHERE ft.file_id = $1
|
||||
JOIN resource_tags rt ON t.id = rt.tag_id
|
||||
WHERE rt.resource_id = $1
|
||||
ORDER BY t.tag_name ASC;
|
||||
|
||||
-- name: GetFilesByTagID :many
|
||||
SELECT f.* FROM files f
|
||||
JOIN file_tags ft ON f.id = ft.file_id
|
||||
WHERE ft.tag_id = $1
|
||||
ORDER BY f.created_at DESC;
|
||||
-- name: GetResourcesByTagID :many
|
||||
SELECT r.* FROM resources r
|
||||
JOIN resource_tags rt ON r.id = rt.resource_id
|
||||
WHERE rt.tag_id = $1
|
||||
ORDER BY r.created_at DESC;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- name: CreateThumbnail :one
|
||||
INSERT INTO thumbnails (file_id, page_number, resolution_label, width, height, storage_key, mime_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetThumbnailsByFileID :many
|
||||
SELECT * FROM thumbnails
|
||||
WHERE file_id = $1
|
||||
ORDER BY page_number ASC, resolution_label ASC;
|
||||
|
||||
-- name: GetThumbnailByID :one
|
||||
SELECT * FROM thumbnails
|
||||
WHERE id = $1 LIMIT 1;
|
||||
|
||||
-- name: DeleteThumbnailsByFileID :exec
|
||||
DELETE FROM thumbnails WHERE file_id = $1;
|
||||
|
||||
-- name: GetThumbnailByFilePageResolution :one
|
||||
SELECT * FROM thumbnails
|
||||
WHERE file_id = $1 AND page_number = $2 AND resolution_label = $3
|
||||
LIMIT 1;
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
// 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 createFolder = `-- name: CreateFolder :one
|
||||
INSERT INTO resources (name, is_folder, owner_id, created_at, updated_at)
|
||||
VALUES ($1, true, $2, 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"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (Resource, error) {
|
||||
row := q.db.QueryRowContext(ctx, createFolder, arg.Name, 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 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
|
||||
`
|
||||
|
||||
func (q *Queries) ListResourcesByOwner(ctx context.Context, ownerID uuid.UUID) ([]Resource, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listResourcesByOwner, ownerID)
|
||||
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
|
||||
`
|
||||
|
||||
type ListResourcesByParentAndOwnerParams struct {
|
||||
ParentResourceID uuid.NullUUID `json:"parent_resource_id"`
|
||||
OwnerID uuid.UUID `json:"owner_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListResourcesByParentAndOwner(ctx context.Context, arg ListResourcesByParentAndOwnerParams) ([]Resource, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listResourcesByParentAndOwner, arg.ParentResourceID, arg.OwnerID)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// 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
|
||||
}
|
||||
@@ -10,41 +10,35 @@ import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const addTagToFile = `-- name: AddTagToFile :exec
|
||||
INSERT INTO file_tags (tag_id, file_id)
|
||||
const addTagToResource = `-- name: AddTagToResource :exec
|
||||
INSERT INTO resource_tags (tag_id, resource_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`
|
||||
|
||||
type AddTagToFileParams struct {
|
||||
TagID sql.NullString `json:"tag_id"`
|
||||
FileID sql.NullString `json:"file_id"`
|
||||
type AddTagToResourceParams struct {
|
||||
TagID sql.NullString `json:"tag_id"`
|
||||
ResourceID sql.NullString `json:"resource_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddTagToFile(ctx context.Context, arg AddTagToFileParams) error {
|
||||
_, err := q.db.ExecContext(ctx, addTagToFile, arg.TagID, arg.FileID)
|
||||
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, tag_type)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, parent_tag_id, tag_name, tag_type, created_at, updated_at
|
||||
INSERT INTO tags (tag_name)
|
||||
VALUES ($1)
|
||||
RETURNING id, parent_tag_id, tag_name, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateTagParams struct {
|
||||
TagName string `json:"tag_name"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateTag(ctx context.Context, arg CreateTagParams) (Tag, error) {
|
||||
row := q.db.QueryRowContext(ctx, createTag, arg.TagName, arg.TagType)
|
||||
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.TagType,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -61,34 +55,34 @@ func (q *Queries) DeleteTag(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const getFilesByTagID = `-- name: GetFilesByTagID :many
|
||||
SELECT f.id, f.name, f.mime_type, f.size, f.storage_key, f.checksum, f.ocr_text, f.created_at, f.updated_at, f.parent_file_id, f.is_folder FROM files f
|
||||
JOIN file_tags ft ON f.id = ft.file_id
|
||||
WHERE ft.tag_id = $1
|
||||
ORDER BY f.created_at DESC
|
||||
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) GetFilesByTagID(ctx context.Context, tagID sql.NullString) ([]File, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getFilesByTagID, tagID)
|
||||
func (q *Queries) GetResourcesByTagID(ctx context.Context, tagID sql.NullString) ([]Resource, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getResourcesByTagID, tagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []File
|
||||
var items []Resource
|
||||
for rows.Next() {
|
||||
var i File
|
||||
var i Resource
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.MimeType,
|
||||
&i.Size,
|
||||
&i.StorageKey,
|
||||
&i.Checksum,
|
||||
&i.OcrText,
|
||||
&i.IsFolder,
|
||||
&i.ParentResourceID,
|
||||
&i.OwnerID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentFileID,
|
||||
&i.IsFolder,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,7 +98,7 @@ func (q *Queries) GetFilesByTagID(ctx context.Context, tagID sql.NullString) ([]
|
||||
}
|
||||
|
||||
const getTag = `-- name: GetTag :one
|
||||
SELECT id, parent_tag_id, tag_name, tag_type, created_at, updated_at FROM tags
|
||||
SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
@@ -115,7 +109,6 @@ func (q *Queries) GetTag(ctx context.Context, id string) (Tag, error) {
|
||||
&i.ID,
|
||||
&i.ParentTagID,
|
||||
&i.TagName,
|
||||
&i.TagType,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
@@ -123,7 +116,7 @@ func (q *Queries) GetTag(ctx context.Context, id string) (Tag, error) {
|
||||
}
|
||||
|
||||
const getTagByName = `-- name: GetTagByName :one
|
||||
SELECT id, parent_tag_id, tag_name, tag_type, created_at, updated_at FROM tags
|
||||
SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
||||
WHERE tag_name = $1
|
||||
`
|
||||
|
||||
@@ -134,22 +127,21 @@ func (q *Queries) GetTagByName(ctx context.Context, tagName string) (Tag, error)
|
||||
&i.ID,
|
||||
&i.ParentTagID,
|
||||
&i.TagName,
|
||||
&i.TagType,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTagsByFileID = `-- name: GetTagsByFileID :many
|
||||
SELECT t.id, t.parent_tag_id, t.tag_name, t.tag_type, t.created_at, t.updated_at FROM tags t
|
||||
JOIN file_tags ft ON t.id = ft.tag_id
|
||||
WHERE ft.file_id = $1
|
||||
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) GetTagsByFileID(ctx context.Context, fileID sql.NullString) ([]Tag, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTagsByFileID, fileID)
|
||||
func (q *Queries) GetTagsByResourceID(ctx context.Context, resourceID sql.NullString) ([]Tag, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTagsByResourceID, resourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -161,7 +153,6 @@ func (q *Queries) GetTagsByFileID(ctx context.Context, fileID sql.NullString) ([
|
||||
&i.ID,
|
||||
&i.ParentTagID,
|
||||
&i.TagName,
|
||||
&i.TagType,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
@@ -179,7 +170,7 @@ func (q *Queries) GetTagsByFileID(ctx context.Context, fileID sql.NullString) ([
|
||||
}
|
||||
|
||||
const listTags = `-- name: ListTags :many
|
||||
SELECT id, parent_tag_id, tag_name, tag_type, created_at, updated_at FROM tags
|
||||
SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
||||
ORDER BY tag_name ASC
|
||||
`
|
||||
|
||||
@@ -196,7 +187,6 @@ func (q *Queries) ListTags(ctx context.Context) ([]Tag, error) {
|
||||
&i.ID,
|
||||
&i.ParentTagID,
|
||||
&i.TagName,
|
||||
&i.TagType,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
@@ -213,17 +203,17 @@ func (q *Queries) ListTags(ctx context.Context) ([]Tag, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeTagFromFile = `-- name: RemoveTagFromFile :exec
|
||||
DELETE FROM file_tags
|
||||
WHERE tag_id = $1 AND file_id = $2
|
||||
const removeTagFromResource = `-- name: RemoveTagFromResource :exec
|
||||
DELETE FROM resource_tags
|
||||
WHERE tag_id = $1 AND resource_id = $2
|
||||
`
|
||||
|
||||
type RemoveTagFromFileParams struct {
|
||||
TagID sql.NullString `json:"tag_id"`
|
||||
FileID sql.NullString `json:"file_id"`
|
||||
type RemoveTagFromResourceParams struct {
|
||||
TagID sql.NullString `json:"tag_id"`
|
||||
ResourceID sql.NullString `json:"resource_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) RemoveTagFromFile(ctx context.Context, arg RemoveTagFromFileParams) error {
|
||||
_, err := q.db.ExecContext(ctx, removeTagFromFile, arg.TagID, arg.FileID)
|
||||
func (q *Queries) RemoveTagFromResource(ctx context.Context, arg RemoveTagFromResourceParams) error {
|
||||
_, err := q.db.ExecContext(ctx, removeTagFromResource, arg.TagID, arg.ResourceID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: thumbnails.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createThumbnail = `-- name: CreateThumbnail :one
|
||||
INSERT INTO thumbnails (file_id, page_number, resolution_label, width, height, storage_key, mime_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, file_id, page_number, resolution_label, width, height, storage_key, mime_type, created_at
|
||||
`
|
||||
|
||||
type CreateThumbnailParams struct {
|
||||
FileID string `json:"file_id"`
|
||||
PageNumber int32 `json:"page_number"`
|
||||
ResolutionLabel string `json:"resolution_label"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateThumbnail(ctx context.Context, arg CreateThumbnailParams) (Thumbnail, error) {
|
||||
row := q.db.QueryRowContext(ctx, createThumbnail,
|
||||
arg.FileID,
|
||||
arg.PageNumber,
|
||||
arg.ResolutionLabel,
|
||||
arg.Width,
|
||||
arg.Height,
|
||||
arg.StorageKey,
|
||||
arg.MimeType,
|
||||
)
|
||||
var i Thumbnail
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FileID,
|
||||
&i.PageNumber,
|
||||
&i.ResolutionLabel,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.StorageKey,
|
||||
&i.MimeType,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteThumbnailsByFileID = `-- name: DeleteThumbnailsByFileID :exec
|
||||
DELETE FROM thumbnails WHERE file_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteThumbnailsByFileID(ctx context.Context, fileID string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteThumbnailsByFileID, fileID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getThumbnailByFilePageResolution = `-- name: GetThumbnailByFilePageResolution :one
|
||||
SELECT id, file_id, page_number, resolution_label, width, height, storage_key, mime_type, created_at FROM thumbnails
|
||||
WHERE file_id = $1 AND page_number = $2 AND resolution_label = $3
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetThumbnailByFilePageResolutionParams struct {
|
||||
FileID string `json:"file_id"`
|
||||
PageNumber int32 `json:"page_number"`
|
||||
ResolutionLabel string `json:"resolution_label"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetThumbnailByFilePageResolution(ctx context.Context, arg GetThumbnailByFilePageResolutionParams) (Thumbnail, error) {
|
||||
row := q.db.QueryRowContext(ctx, getThumbnailByFilePageResolution, arg.FileID, arg.PageNumber, arg.ResolutionLabel)
|
||||
var i Thumbnail
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FileID,
|
||||
&i.PageNumber,
|
||||
&i.ResolutionLabel,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.StorageKey,
|
||||
&i.MimeType,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getThumbnailByID = `-- name: GetThumbnailByID :one
|
||||
SELECT id, file_id, page_number, resolution_label, width, height, storage_key, mime_type, created_at FROM thumbnails
|
||||
WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetThumbnailByID(ctx context.Context, id string) (Thumbnail, error) {
|
||||
row := q.db.QueryRowContext(ctx, getThumbnailByID, id)
|
||||
var i Thumbnail
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FileID,
|
||||
&i.PageNumber,
|
||||
&i.ResolutionLabel,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.StorageKey,
|
||||
&i.MimeType,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getThumbnailsByFileID = `-- name: GetThumbnailsByFileID :many
|
||||
SELECT id, file_id, page_number, resolution_label, width, height, storage_key, mime_type, created_at FROM thumbnails
|
||||
WHERE file_id = $1
|
||||
ORDER BY page_number ASC, resolution_label ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetThumbnailsByFileID(ctx context.Context, fileID string) ([]Thumbnail, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getThumbnailsByFileID, fileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Thumbnail
|
||||
for rows.Next() {
|
||||
var i Thumbnail
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FileID,
|
||||
&i.PageNumber,
|
||||
&i.ResolutionLabel,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.StorageKey,
|
||||
&i.MimeType,
|
||||
&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
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -6,17 +6,37 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
File *FileHandler
|
||||
OCR *OCRHandler
|
||||
Health *HealthHandler
|
||||
Auth *auth.AuthHandler
|
||||
Resource *ResourceHandler
|
||||
OCR *OCRHandler
|
||||
Health *HealthHandler
|
||||
Auth *auth.AuthHandler
|
||||
Share *ShareHandler
|
||||
Device *DeviceHandler
|
||||
Sync *SyncHandler
|
||||
}
|
||||
|
||||
func New(fileSvc *service.FileService, ocrSvc *service.OCRService, urlSvc *service.URLService, authHandler *auth.AuthHandler, conversionSvc *service.ConversionService) *Handler {
|
||||
func New(
|
||||
resourceSvc *service.ResourceService,
|
||||
ocrSvc *service.OCRService,
|
||||
urlSvc *service.URLService,
|
||||
authHandler *auth.AuthHandler,
|
||||
conversionSvc *service.ConversionService,
|
||||
rebacSvc *service.RebacService,
|
||||
placementSvc *service.PlacementService,
|
||||
syncSvc *service.SyncService,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
File: &FileHandler{files: fileSvc, urls: urlSvc, ocr: ocrSvc, conversion: conversionSvc},
|
||||
OCR: &OCRHandler{ocr: ocrSvc, files: fileSvc},
|
||||
Resource: &ResourceHandler{
|
||||
resources: resourceSvc,
|
||||
urls: urlSvc,
|
||||
ocr: ocrSvc,
|
||||
conversion: conversionSvc,
|
||||
},
|
||||
OCR: &OCRHandler{ocr: ocrSvc, resources: resourceSvc},
|
||||
Health: &HealthHandler{ocr: ocrSvc},
|
||||
Auth: authHandler,
|
||||
Share: &ShareHandler{rebac: rebacSvc},
|
||||
Device: &DeviceHandler{placement: placementSvc},
|
||||
Sync: &SyncHandler{sync: syncSvc},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
type OCRHandler struct {
|
||||
ocr *service.OCRService
|
||||
files *service.FileService
|
||||
ocr *service.OCRService
|
||||
resources *service.ResourceService
|
||||
}
|
||||
|
||||
func (h *OCRHandler) CreateJob(c *gin.Context) {
|
||||
|
||||
@@ -8,18 +8,21 @@ import (
|
||||
"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 FileHandler struct {
|
||||
files *service.FileService
|
||||
type ResourceHandler struct {
|
||||
resources *service.ResourceService
|
||||
urls *service.URLService
|
||||
ocr *service.OCRService
|
||||
conversion *service.ConversionService
|
||||
}
|
||||
|
||||
func (h *FileHandler) Upload(c *gin.Context) {
|
||||
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")
|
||||
@@ -34,7 +37,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
|
||||
results := make([]gin.H, 0, len(files))
|
||||
for _, file := range files {
|
||||
result, err := h.files.Upload(file)
|
||||
result, err := h.resources.Upload(file, userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "UPLOAD_ERROR", err.Error())
|
||||
return
|
||||
@@ -55,23 +58,23 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
api.Success(c, results)
|
||||
}
|
||||
|
||||
func (h *FileHandler) List(c *gin.Context) {
|
||||
func (h *ResourceHandler) List(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
thumbnailQuality := c.Query("thumbnail")
|
||||
|
||||
files, err := h.files.List()
|
||||
resources, err := h.resources.List(userID)
|
||||
if err != nil {
|
||||
log.Printf("ERROR List files: %v", err)
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files")
|
||||
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"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
type fileResponse struct {
|
||||
type resourceResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
||||
@@ -81,50 +84,51 @@ func (h *FileHandler) List(c *gin.Context) {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
MimeType string `json:"mimeType"`
|
||||
OcrText string `json:"ocrText,omitempty"`
|
||||
ParentFileID string `json:"parentFileID,omitempty"`
|
||||
ParentID string `json:"parentResourceId,omitempty"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
OwnerID string `json:"ownerId"`
|
||||
}
|
||||
|
||||
resp := make([]fileResponse, len(files))
|
||||
for i, f := range files {
|
||||
resp := make([]resourceResponse, len(resources))
|
||||
for i, r := range resources {
|
||||
tags := []tagResponse{}
|
||||
for _, tag := range f.Tags {
|
||||
for _, tag := range r.Tags {
|
||||
tags = append(tags, tagResponse{
|
||||
ID: tag.ID,
|
||||
TagName: tag.Name,
|
||||
TagType: tag.TagType,
|
||||
})
|
||||
}
|
||||
|
||||
downloadURL := h.urls.GenerateDownloadURL(f.ID)
|
||||
downloadURL := h.urls.GenerateDownloadURL(r.ID)
|
||||
thumbURL := downloadURL
|
||||
if thumbnailQuality != "" {
|
||||
if best := h.files.GetBestThumbnail(f.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
|
||||
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
||||
}
|
||||
}
|
||||
|
||||
resp[i] = fileResponse{
|
||||
ID: f.ID,
|
||||
resp[i] = resourceResponse{
|
||||
ID: r.ID,
|
||||
URL: downloadURL,
|
||||
ThumbnailURL: thumbURL,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Name: r.Name,
|
||||
Size: r.Size,
|
||||
Tags: tags,
|
||||
CreatedAt: f.CreatedAt,
|
||||
ParentFileID: f.ParentFileID,
|
||||
OcrText: f.OcrText,
|
||||
IsFolder: f.IsFolder,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
MimeType: f.MimeType,
|
||||
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, 1, len(resp))
|
||||
}
|
||||
|
||||
func (h *FileHandler) Download(c *gin.Context) {
|
||||
func (h *ResourceHandler) Download(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
|
||||
sig := c.Query("sig")
|
||||
@@ -134,133 +138,126 @@ func (h *FileHandler) Download(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
storagePath, err := h.files.GetStoragePath(id)
|
||||
storagePath, err := h.resources.GetStoragePath(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
|
||||
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
|
||||
return
|
||||
}
|
||||
|
||||
c.File(path.Clean(storagePath))
|
||||
}
|
||||
|
||||
func (h *FileHandler) Get(c *gin.Context) {
|
||||
func (h *ResourceHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
thumbnailQuality := c.Query("thumbnail")
|
||||
|
||||
file, err := h.files.Get(id)
|
||||
resource, err := h.resources.Get(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
|
||||
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
|
||||
return
|
||||
}
|
||||
|
||||
type tagResponse struct {
|
||||
ID string `json:"id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
type thumbnailResponse struct {
|
||||
ID string `json:"id"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
ResolutionLabel string `json:"resolutionLabel"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mimeType"`
|
||||
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 file.Tags {
|
||||
for _, tag := range resource.Tags {
|
||||
tags = append(tags, tagResponse{
|
||||
ID: tag.ID,
|
||||
TagName: tag.Name,
|
||||
TagType: tag.TagType,
|
||||
})
|
||||
}
|
||||
|
||||
downloadURL := h.urls.GenerateDownloadURL(file.ID)
|
||||
downloadURL := h.urls.GenerateDownloadURL(resource.ID)
|
||||
thumbURL := downloadURL
|
||||
if thumbnailQuality != "" {
|
||||
if best := h.files.GetBestThumbnail(file.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
|
||||
if best := h.resources.GetBestVariant(resource.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
||||
}
|
||||
}
|
||||
|
||||
dbThumbnails, _ := h.files.GetThumbnailsByFileID(file.ID)
|
||||
thumbnails := make([]thumbnailResponse, len(dbThumbnails))
|
||||
for i, t := range dbThumbnails {
|
||||
thumbnails[i] = thumbnailResponse{
|
||||
ID: t.ID,
|
||||
PageNumber: t.PageNumber,
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: t.Width,
|
||||
Height: t.Height,
|
||||
URL: h.urls.GenerateThumbnailURL(t.ID),
|
||||
MimeType: t.MimeType,
|
||||
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": file.ID,
|
||||
"name": file.Name,
|
||||
"id": resource.ID,
|
||||
"name": resource.Name,
|
||||
"url": downloadURL,
|
||||
"thumbnailUrl": thumbURL,
|
||||
"size": file.Size,
|
||||
"mimeType": file.MimeType,
|
||||
"size": resource.Size,
|
||||
"mimeType": resource.MimeType,
|
||||
"tags": tags,
|
||||
"createdAt": file.CreatedAt,
|
||||
"updatedAt": file.UpdatedAt,
|
||||
"ocrText": file.OcrText,
|
||||
"isFolder": file.IsFolder,
|
||||
"parentFileID": file.ParentFileID,
|
||||
"thumbnails": thumbnails,
|
||||
"createdAt": resource.CreatedAt,
|
||||
"updatedAt": resource.UpdatedAt,
|
||||
"ocrText": resource.OcrText,
|
||||
"isFolder": resource.IsFolder,
|
||||
"parentResourceId": resource.ParentResourceID,
|
||||
"ownerId": resource.OwnerID,
|
||||
"variants": variants,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FileHandler) Delete(c *gin.Context) {
|
||||
func (h *ResourceHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
storagePath, _ := h.files.GetStoragePath(id)
|
||||
thumbnails, _ := h.files.GetThumbnailsByFileID(id)
|
||||
storagePath, _ := h.resources.GetStoragePath(id)
|
||||
variants, _ := h.resources.GetVariantsByResourceID(id)
|
||||
|
||||
if err := h.files.Delete(id); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete file")
|
||||
if err := h.resources.Delete(id); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete resource")
|
||||
return
|
||||
}
|
||||
|
||||
if storagePath != "" {
|
||||
os.Remove(path.Clean(storagePath))
|
||||
}
|
||||
for _, t := range thumbnails {
|
||||
os.Remove(path.Clean(t.StorageKey))
|
||||
for _, v := range variants {
|
||||
os.Remove(path.Clean(v.StorageKey))
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *FileHandler) AddTags(c *gin.Context) {
|
||||
func (h *ResourceHandler) AddTags(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var body struct {
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
TagType string `json:"tag_type"`
|
||||
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
|
||||
}
|
||||
|
||||
tagType := body.TagType
|
||||
if tagType == "" {
|
||||
tagType = "none"
|
||||
}
|
||||
|
||||
if err := h.files.AddTags(id, body.Tags, tagType); err != nil {
|
||||
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.files.GetTagsByFileID(id)
|
||||
tags, err := h.resources.GetTagsByResourceID(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
|
||||
return
|
||||
@@ -269,9 +266,9 @@ func (h *FileHandler) AddTags(c *gin.Context) {
|
||||
api.Success(c, tags)
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetTags(c *gin.Context) {
|
||||
func (h *ResourceHandler) GetTags(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
tags, err := h.files.GetTagsByFileID(id)
|
||||
tags, err := h.resources.GetTagsByResourceID(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
|
||||
return
|
||||
@@ -279,26 +276,27 @@ func (h *FileHandler) GetTags(c *gin.Context) {
|
||||
api.Success(c, tags)
|
||||
}
|
||||
|
||||
func (h *FileHandler) MoveFiles(c *gin.Context) {
|
||||
func (h *ResourceHandler) MoveResources(c *gin.Context) {
|
||||
var body struct {
|
||||
FileIDs []string `json:"file_ids" binding:"required"`
|
||||
ParentFileID *string `json:"parent_file_id"`
|
||||
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 'file_ids' array")
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'resource_ids' array")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.files.MoveFiles(body.FileIDs, body.ParentFileID); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to move files")
|
||||
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.FileIDs)})
|
||||
api.Success(c, gin.H{"moved": len(body.ResourceIDs)})
|
||||
}
|
||||
|
||||
func (h *FileHandler) ListFolders(c *gin.Context) {
|
||||
folders, err := h.files.ListFolders()
|
||||
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
|
||||
@@ -306,7 +304,9 @@ func (h *FileHandler) ListFolders(c *gin.Context) {
|
||||
api.Success(c, folders)
|
||||
}
|
||||
|
||||
func (h *FileHandler) CreateFolder(c *gin.Context) {
|
||||
func (h *ResourceHandler) CreateFolder(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func (h *FileHandler) CreateFolder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
folder, err := h.files.CreateFolder(body.Name)
|
||||
folder, err := h.resources.CreateFolder(body.Name, userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to create folder")
|
||||
return
|
||||
@@ -324,23 +324,23 @@ func (h *FileHandler) CreateFolder(c *gin.Context) {
|
||||
api.Success(c, folder)
|
||||
}
|
||||
|
||||
func (h *FileHandler) ListFilesByParent(c *gin.Context) {
|
||||
func (h *ResourceHandler) ListByParent(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
parentID := c.Param("id")
|
||||
thumbnailQuality := c.Query("thumbnail")
|
||||
|
||||
files, err := h.files.ListFilesByParentID(parentID)
|
||||
resources, err := h.resources.ListResourcesByParentID(parentID, userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files in folder")
|
||||
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"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
type fileResponse struct {
|
||||
type resourceResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
||||
@@ -350,84 +350,83 @@ func (h *FileHandler) ListFilesByParent(c *gin.Context) {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
MimeType string `json:"mimeType"`
|
||||
OcrText string `json:"ocrText,omitempty"`
|
||||
ParentFileID string `json:"parentFileID,omitempty"`
|
||||
ParentID string `json:"parentResourceId,omitempty"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
resp := make([]fileResponse, len(files))
|
||||
for i, f := range files {
|
||||
resp := make([]resourceResponse, len(resources))
|
||||
for i, r := range resources {
|
||||
tags := []tagResponse{}
|
||||
for _, tag := range f.Tags {
|
||||
for _, tag := range r.Tags {
|
||||
tags = append(tags, tagResponse{
|
||||
ID: tag.ID,
|
||||
TagName: tag.Name,
|
||||
TagType: tag.TagType,
|
||||
})
|
||||
}
|
||||
|
||||
downloadURL := h.urls.GenerateDownloadURL(f.ID)
|
||||
downloadURL := h.urls.GenerateDownloadURL(r.ID)
|
||||
thumbURL := downloadURL
|
||||
if thumbnailQuality != "" {
|
||||
if best := h.files.GetBestThumbnail(f.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
|
||||
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
||||
}
|
||||
}
|
||||
|
||||
resp[i] = fileResponse{
|
||||
ID: f.ID,
|
||||
resp[i] = resourceResponse{
|
||||
ID: r.ID,
|
||||
URL: downloadURL,
|
||||
ThumbnailURL: thumbURL,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Name: r.Name,
|
||||
Size: r.Size,
|
||||
Tags: tags,
|
||||
CreatedAt: f.CreatedAt,
|
||||
ParentFileID: f.ParentFileID,
|
||||
OcrText: f.OcrText,
|
||||
IsFolder: f.IsFolder,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
MimeType: f.MimeType,
|
||||
CreatedAt: r.CreatedAt.String(),
|
||||
ParentID: r.ParentResourceID,
|
||||
OcrText: r.OcrText,
|
||||
IsFolder: r.IsFolder,
|
||||
UpdatedAt: r.UpdatedAt.String(),
|
||||
MimeType: r.MimeType,
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetThumbnails(c *gin.Context) {
|
||||
func (h *ResourceHandler) GetVariants(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
thumbnails, err := h.files.GetThumbnailsByFileID(id)
|
||||
variants, err := h.resources.GetVariantsByResourceID(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch thumbnails")
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch variants")
|
||||
return
|
||||
}
|
||||
|
||||
type thumbnailResponse struct {
|
||||
ID string `json:"id"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
ResolutionLabel string `json:"resolutionLabel"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mimeType"`
|
||||
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([]thumbnailResponse, len(thumbnails))
|
||||
for i, t := range thumbnails {
|
||||
resp[i] = thumbnailResponse{
|
||||
ID: t.ID,
|
||||
PageNumber: t.PageNumber,
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: t.Width,
|
||||
Height: t.Height,
|
||||
URL: h.urls.GenerateThumbnailURL(t.ID),
|
||||
MimeType: t.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 *FileHandler) ServeThumbnail(c *gin.Context) {
|
||||
func (h *ResourceHandler) ServeVariant(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
|
||||
sig := c.Query("sig")
|
||||
@@ -437,16 +436,16 @@ func (h *FileHandler) ServeThumbnail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
storagePath, err := h.files.GetThumbnailStoragePath(id)
|
||||
storagePath, err := h.resources.GetVariantStoragePath(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusNotFound, "THUMBNAIL_NOT_FOUND", "Thumbnail not found")
|
||||
api.Error(c, http.StatusNotFound, "VARIANT_NOT_FOUND", "Variant not found")
|
||||
return
|
||||
}
|
||||
|
||||
c.File(path.Clean(storagePath))
|
||||
}
|
||||
|
||||
func (h *FileHandler) CheckDuplicates(c *gin.Context) {
|
||||
func (h *ResourceHandler) CheckDuplicates(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Size int64 `json:"size" binding:"required"`
|
||||
@@ -457,7 +456,7 @@ func (h *FileHandler) CheckDuplicates(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
duplicates, err := h.files.FindDuplicatesByNameSize(body.Name, body.Size)
|
||||
duplicates, err := h.resources.FindDuplicatesByNameSize(body.Name, body.Size)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to check duplicates")
|
||||
return
|
||||
@@ -475,7 +474,7 @@ func (h *FileHandler) CheckDuplicates(c *gin.Context) {
|
||||
resp := make([]dupResponse, len(duplicates))
|
||||
for i, d := range duplicates {
|
||||
resp[i] = dupResponse{
|
||||
ID: d.ID,
|
||||
ID: d.ID.String(),
|
||||
Name: d.Name,
|
||||
MimeType: d.MimeType,
|
||||
Size: d.Size,
|
||||
@@ -14,29 +14,65 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) {
|
||||
api.POST("/auth/login", h.Auth.Login)
|
||||
api.POST("/auth/refresh", h.Auth.Refresh)
|
||||
api.POST("/auth/logout", h.Auth.Logout)
|
||||
api.GET("/files/download/:id", h.File.Download)
|
||||
api.GET("/thumbnails/:id", h.File.ServeThumbnail)
|
||||
api.GET("/resources/download/:id", h.Resource.Download)
|
||||
api.GET("/variants/:id", h.Resource.ServeVariant)
|
||||
|
||||
// Protected
|
||||
protected := api.Group("")
|
||||
protected.Use(authMiddleware.RequireAuth())
|
||||
|
||||
protected.GET("/files", h.File.List)
|
||||
protected.POST("/files/upload", h.File.Upload)
|
||||
protected.POST("/files/move", h.File.MoveFiles)
|
||||
protected.POST("/files/folders", h.File.CreateFolder)
|
||||
protected.GET("/files/folders", h.File.ListFolders)
|
||||
protected.GET("/files/folders/:id/files", h.File.ListFilesByParent)
|
||||
protected.DELETE("/files/:id", h.File.Delete)
|
||||
protected.GET("/files/:id", h.File.Get)
|
||||
// Resources (replaces /files)
|
||||
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)
|
||||
|
||||
protected.POST("/files/:id/tags", h.File.AddTags)
|
||||
protected.GET("/files/:id/tags", h.File.GetTags)
|
||||
// Tags on resources
|
||||
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)
|
||||
|
||||
// Dedup check
|
||||
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)
|
||||
|
||||
// Device management
|
||||
protected.GET("/devices", h.Device.List)
|
||||
protected.POST("/devices", h.Device.Register)
|
||||
|
||||
// Placements
|
||||
protected.GET("/resources/:id/placements", h.Resource.GetVariants)
|
||||
|
||||
// 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)
|
||||
|
||||
protected.GET("/files/:id/thumbnails", h.File.GetThumbnails)
|
||||
|
||||
protected.POST("/files/dedup-check", h.File.CheckDuplicates)
|
||||
// Legacy /files/* endpoints (maintain backward compatibility)
|
||||
protected.GET("/files", h.Resource.List)
|
||||
protected.POST("/files/upload", h.Resource.Upload)
|
||||
protected.POST("/files/move", h.Resource.MoveResources)
|
||||
protected.POST("/files/folders", h.Resource.CreateFolder)
|
||||
protected.GET("/files/folders", h.Resource.ListFolders)
|
||||
protected.GET("/files/folders/:id/files", h.Resource.ListByParent)
|
||||
protected.DELETE("/files/:id", h.Resource.Delete)
|
||||
protected.GET("/files/:id", h.Resource.Get)
|
||||
protected.POST("/files/:id/tags", h.Resource.AddTags)
|
||||
protected.GET("/files/:id/tags", h.Resource.GetTags)
|
||||
protected.GET("/files/:id/thumbnails", h.Resource.GetVariants)
|
||||
protected.POST("/files/dedup-check", h.Resource.CheckDuplicates)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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 != "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vaultdrop/backend/internal/service"
|
||||
"github.com/vaultdrop/backend/pkg/api"
|
||||
)
|
||||
|
||||
type SyncHandler struct {
|
||||
sync *service.SyncService
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Pull(c *gin.Context) {
|
||||
var body struct {
|
||||
LocationID string `json:"location_id"`
|
||||
}
|
||||
c.ShouldBindJSON(&body)
|
||||
|
||||
var err error
|
||||
var items interface{}
|
||||
if body.LocationID != "" {
|
||||
items, err = h.sync.ListPending(body.LocationID)
|
||||
} else {
|
||||
items, err = h.sync.ListAllPending()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, items)
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Push(c *gin.Context) {
|
||||
var body struct {
|
||||
LocationID string `json:"location_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "location_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.sync.ListPending(body.LocationID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
|
||||
return
|
||||
}
|
||||
|
||||
api.Created(c, gin.H{
|
||||
"pending": len(items),
|
||||
"message": "Push initiated",
|
||||
})
|
||||
}
|
||||
@@ -1,24 +1,39 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Tag struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
TagType string `json:"tagType"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Size int64 `json:"size"`
|
||||
StorageKey string `json:"-"`
|
||||
Checksum string `json:"-"`
|
||||
OcrText string `json:"ocrText,omitempty"`
|
||||
Tags []Tag `json:"tags"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
ParentFileID string `json:"parentFileId,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
type Resource struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Size int64 `json:"size"`
|
||||
StorageKey string `json:"-"`
|
||||
Checksum string `json:"-"`
|
||||
OcrText string `json:"ocrText,omitempty"`
|
||||
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 {
|
||||
|
||||
@@ -1,13 +1 @@
|
||||
package model
|
||||
|
||||
type Thumbnail struct {
|
||||
ID string `json:"id"`
|
||||
FileID string `json:"fileId"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
ResolutionLabel string `json:"resolutionLabel"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
StorageKey string `json:"-"`
|
||||
MimeType string `json:"mimeType"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
@@ -2,18 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
func CreateSHA256Hash(data []byte) []byte {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(data)
|
||||
return hasher.Sum(nil)
|
||||
}
|
||||
|
||||
func CompareHash(x, y []byte) bool {
|
||||
if len(x) != len(y) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare(x, y) == 1
|
||||
h := sha256.Sum256(data)
|
||||
return h[:]
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
)
|
||||
|
||||
type ConversionJob struct {
|
||||
FileID string
|
||||
FilePath string
|
||||
MimeType string
|
||||
ResourceID string
|
||||
FilePath string
|
||||
MimeType string
|
||||
}
|
||||
|
||||
type ConversionService struct {
|
||||
@@ -46,9 +46,9 @@ func (s *ConversionService) Stop() {
|
||||
log.Println("[Conversion] Worker stopped")
|
||||
}
|
||||
|
||||
func (s *ConversionService) Enqueue(fileID, filePath, mimeType string) {
|
||||
s.jobs <- ConversionJob{FileID: fileID, FilePath: filePath, MimeType: mimeType}
|
||||
log.Printf("[Conversion] Enqueued file %s", fileID)
|
||||
func (s *ConversionService) Enqueue(resourceID, filePath, mimeType string) {
|
||||
s.jobs <- ConversionJob{ResourceID: resourceID, FilePath: filePath, MimeType: mimeType}
|
||||
log.Printf("[Conversion] Enqueued resource %s", resourceID)
|
||||
}
|
||||
|
||||
func (s *ConversionService) worker() {
|
||||
@@ -58,7 +58,7 @@ func (s *ConversionService) worker() {
|
||||
}
|
||||
|
||||
func (s *ConversionService) process(job ConversionJob) {
|
||||
log.Printf("[Conversion] Processing file %s (mime: %s)", job.FileID, job.MimeType)
|
||||
log.Printf("[Conversion] Processing resource %s (mime: %s)", job.ResourceID, job.MimeType)
|
||||
|
||||
pdfPath := job.FilePath
|
||||
tmpDir := ""
|
||||
@@ -67,18 +67,18 @@ func (s *ConversionService) process(job ConversionJob) {
|
||||
var err error
|
||||
pdfPath, tmpDir, err = s.convertToPDF(job.FilePath)
|
||||
if err != nil {
|
||||
log.Printf("[Conversion] Failed to convert file %s to PDF: %v", job.FileID, err)
|
||||
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 file %s: unsupported mime type %s", job.FileID, job.MimeType)
|
||||
log.Printf("[Conversion] Skipping resource %s: unsupported mime type %s", job.ResourceID, job.MimeType)
|
||||
return
|
||||
}
|
||||
|
||||
thumbDir := filepath.Join(s.cfg.ThumbnailDir, job.FileID)
|
||||
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.FileID, err)
|
||||
log.Printf("[Conversion] Failed to create thumbnail dir for %s: %v", job.ResourceID, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,14 +86,14 @@ func (s *ConversionService) process(job ConversionJob) {
|
||||
label string
|
||||
dpi int
|
||||
}{
|
||||
{"thumbnail", 21},
|
||||
{"full", 200},
|
||||
{"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 file %s to images (res=%s): %v", job.FileID, res.label, err)
|
||||
log.Printf("[Conversion] Failed to convert resource %s to images (res=%s): %v", job.ResourceID, res.label, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -104,32 +104,33 @@ func (s *ConversionService) process(job ConversionJob) {
|
||||
width, height = 0, 0
|
||||
}
|
||||
|
||||
thumbUUID := uuid.New().String()
|
||||
dstPath := filepath.Join(thumbDir, thumbUUID+".jpg")
|
||||
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
|
||||
}
|
||||
|
||||
_, err = s.queries.CreateThumbnail(context.Background(), db.CreateThumbnailParams{
|
||||
FileID: job.FileID,
|
||||
PageNumber: int32(page.number),
|
||||
ResolutionLabel: res.label,
|
||||
Width: int32(width),
|
||||
Height: int32(height),
|
||||
StorageKey: dstPath,
|
||||
MimeType: "image/jpeg",
|
||||
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 thumbnail record for file %s page %d: %v", job.FileID, page.number, err)
|
||||
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 file %s", len(pages), res.label, job.FileID)
|
||||
log.Printf("[Conversion] Generated %d %s images for resource %s", len(pages), res.label, job.ResourceID)
|
||||
}
|
||||
|
||||
log.Printf("[Conversion] Completed file %s", job.FileID)
|
||||
log.Printf("[Conversion] Completed resource %s", job.ResourceID)
|
||||
}
|
||||
|
||||
func (s *ConversionService) convertToPDF(inputPath string) (string, string, error) {
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/vaultdrop/backend/internal/config"
|
||||
"github.com/vaultdrop/backend/internal/db"
|
||||
"github.com/vaultdrop/backend/internal/model"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
queries *db.Queries
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewFileService(queries *db.Queries, cfg *config.Config) *FileService {
|
||||
return &FileService{queries: queries, cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *FileService) Upload(file *multipart.FileHeader) (*model.UploadResult, error) {
|
||||
dst := filepath.Join(s.cfg.UploadDir, uuid.New().String()+filepath.Ext(file.Filename))
|
||||
|
||||
if err := os.MkdirAll(s.cfg.UploadDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create upload dir: %w", err)
|
||||
}
|
||||
|
||||
if err := saveUploadedFile(file, dst); err != nil {
|
||||
return nil, fmt.Errorf("save file: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read saved file: %w", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat file: %w", err)
|
||||
}
|
||||
|
||||
checksum := hex.EncodeToString(CreateSHA256Hash(data))
|
||||
|
||||
existing, err := s.queries.FindDuplicateByChecksum(context.Background(), checksum)
|
||||
if err == nil && existing.ID != "" {
|
||||
os.Remove(dst)
|
||||
return &model.UploadResult{
|
||||
ID: existing.ID,
|
||||
Name: existing.Name,
|
||||
Path: existing.StorageKey,
|
||||
MimeType: existing.MimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
dbFile, err := s.queries.CreateFile(context.Background(), db.CreateFileParams{
|
||||
Name: file.Filename,
|
||||
MimeType: file.Header.Get("Content-Type"),
|
||||
Size: info.Size(),
|
||||
StorageKey: dst,
|
||||
Checksum: checksum,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create file in db: %w", err)
|
||||
}
|
||||
|
||||
return &model.UploadResult{
|
||||
ID: dbFile.ID,
|
||||
Name: dbFile.Name,
|
||||
Path: dst,
|
||||
MimeType: dbFile.MimeType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FileService) List() ([]model.File, error) {
|
||||
dbFiles, err := s.queries.ListFiles(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list files: %w", err)
|
||||
}
|
||||
|
||||
files := make([]model.File, len(dbFiles))
|
||||
for i, f := range dbFiles {
|
||||
tags, err := s.queries.GetTagsByFileID(context.Background(), sql.NullString{String: f.ID, Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get tags for file %s: %w", f.ID, err)
|
||||
}
|
||||
files[i] = dbToModel(f, tags)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *FileService) Get(id string) (*model.File, error) {
|
||||
f, err := s.queries.GetFile(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get file: %w", err)
|
||||
}
|
||||
tags, err := s.queries.GetTagsByFileID(context.Background(), sql.NullString{String: f.ID, Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get tags: %w", err)
|
||||
}
|
||||
m := dbToModel(f, tags)
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (s *FileService) Delete(id string) error {
|
||||
return s.queries.DeleteFile(context.Background(), id)
|
||||
}
|
||||
|
||||
func (s *FileService) GetStoragePath(id string) (string, error) {
|
||||
f, err := s.queries.GetFile(context.Background(), id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get file: %w", err)
|
||||
}
|
||||
return f.StorageKey, nil
|
||||
}
|
||||
|
||||
func (s *FileService) UpdateOCRText(id, text string) error {
|
||||
f, err := s.queries.GetFile(context.Background(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get file: %w", err)
|
||||
}
|
||||
return s.queries.UpdateFile(context.Background(), db.UpdateFileParams{
|
||||
Name: f.Name,
|
||||
MimeType: f.MimeType,
|
||||
OcrText: text,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileService) AddTags(fileID string, tagNames []string, tagType string) error {
|
||||
for _, name := range tagNames {
|
||||
tag, err := s.queries.GetTagByName(context.Background(), name)
|
||||
if err == sql.ErrNoRows {
|
||||
tag, err = s.queries.CreateTag(context.Background(), db.CreateTagParams{
|
||||
TagName: name,
|
||||
TagType: tagType,
|
||||
})
|
||||
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.AddTagToFile(context.Background(), db.AddTagToFileParams{
|
||||
TagID: sql.NullString{String: tag.ID, Valid: true},
|
||||
FileID: sql.NullString{String: fileID, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("link tag %q to file: %w", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileService) GetTagsByFileID(fileID string) ([]model.Tag, error) {
|
||||
dbTags, err := s.queries.GetTagsByFileID(context.Background(), sql.NullString{String: fileID, Valid: true})
|
||||
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, Name: t.TagName, TagType: t.TagType}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (s *FileService) MoveFiles(fileIDs []string, parentFileID *string) error {
|
||||
var parentID sql.NullString
|
||||
if parentFileID != nil {
|
||||
parentID = sql.NullString{String: *parentFileID, Valid: true}
|
||||
}
|
||||
return s.queries.MoveFiles(context.Background(), db.MoveFilesParams{
|
||||
ParentFileID: parentID,
|
||||
Column2: fileIDs,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileService) CreateFolder(name string) (*model.File, error) {
|
||||
f, err := s.queries.CreateFolder(context.Background(), name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create folder: %w", err)
|
||||
}
|
||||
m := dbToModel(f, nil)
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (s *FileService) ListFolders() ([]model.File, error) {
|
||||
dbFiles, err := s.queries.ListFolders(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list folders: %w", err)
|
||||
}
|
||||
folders := make([]model.File, len(dbFiles))
|
||||
for i, f := range dbFiles {
|
||||
folders[i] = dbToModel(f, nil)
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (s *FileService) ListFilesByParentID(parentID string) ([]model.File, error) {
|
||||
dbFiles, err := s.queries.ListFilesByParentID(context.Background(), sql.NullString{String: parentID, Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list files by parent: %w", err)
|
||||
}
|
||||
files := make([]model.File, len(dbFiles))
|
||||
for i, f := range dbFiles {
|
||||
tags, err := s.queries.GetTagsByFileID(context.Background(), sql.NullString{String: f.ID, Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get tags for file %s: %w", f.ID, err)
|
||||
}
|
||||
files[i] = dbToModel(f, tags)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *FileService) GetThumbnailsByFileID(fileID string) ([]model.Thumbnail, error) {
|
||||
dbThumbnails, err := s.queries.GetThumbnailsByFileID(context.Background(), fileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get thumbnails: %w", err)
|
||||
}
|
||||
|
||||
thumbnails := make([]model.Thumbnail, len(dbThumbnails))
|
||||
for i, t := range dbThumbnails {
|
||||
thumbnails[i] = model.Thumbnail{
|
||||
ID: t.ID,
|
||||
FileID: t.FileID,
|
||||
PageNumber: int(t.PageNumber),
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: int(t.Width),
|
||||
Height: int(t.Height),
|
||||
StorageKey: t.StorageKey,
|
||||
MimeType: t.MimeType,
|
||||
CreatedAt: t.CreatedAt.String(),
|
||||
}
|
||||
}
|
||||
return thumbnails, nil
|
||||
}
|
||||
|
||||
func (s *FileService) GetThumbnailStoragePath(id string) (string, error) {
|
||||
t, err := s.queries.GetThumbnailByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get thumbnail: %w", err)
|
||||
}
|
||||
return t.StorageKey, nil
|
||||
}
|
||||
|
||||
func (s *FileService) GetBestThumbnail(fileID, preferredLabel string) *model.Thumbnail {
|
||||
dbThumbnails, err := s.queries.GetThumbnailsByFileID(context.Background(), fileID)
|
||||
if err != nil || len(dbThumbnails) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fallback *model.Thumbnail
|
||||
for _, t := range dbThumbnails {
|
||||
if t.PageNumber != 1 {
|
||||
continue
|
||||
}
|
||||
if t.ResolutionLabel == preferredLabel {
|
||||
return &model.Thumbnail{
|
||||
ID: t.ID,
|
||||
FileID: t.FileID,
|
||||
PageNumber: int(t.PageNumber),
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: int(t.Width),
|
||||
Height: int(t.Height),
|
||||
StorageKey: t.StorageKey,
|
||||
MimeType: t.MimeType,
|
||||
CreatedAt: t.CreatedAt.String(),
|
||||
}
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = &model.Thumbnail{
|
||||
ID: t.ID,
|
||||
FileID: t.FileID,
|
||||
PageNumber: int(t.PageNumber),
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: int(t.Width),
|
||||
Height: int(t.Height),
|
||||
StorageKey: t.StorageKey,
|
||||
MimeType: t.MimeType,
|
||||
CreatedAt: t.CreatedAt.String(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (s *FileService) FindDuplicatesByNameSize(name string, size int64) ([]db.FindDuplicatesByNameSizeRow, error) {
|
||||
return s.queries.FindDuplicatesByNameSize(context.Background(), db.FindDuplicatesByNameSizeParams{
|
||||
Name: name,
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
|
||||
func dbToModel(f db.File, dbTags []db.Tag) model.File {
|
||||
tags := make([]model.Tag, len(dbTags))
|
||||
for i, t := range dbTags {
|
||||
tags[i] = model.Tag{ID: t.ID, Name: t.TagName, TagType: t.TagType}
|
||||
}
|
||||
|
||||
return model.File{
|
||||
ID: f.ID,
|
||||
Name: f.Name,
|
||||
MimeType: f.MimeType,
|
||||
Size: f.Size,
|
||||
StorageKey: f.StorageKey,
|
||||
Checksum: f.Checksum,
|
||||
OcrText: f.OcrText,
|
||||
IsFolder: f.IsFolder,
|
||||
ParentFileID: f.ParentFileID.String,
|
||||
Tags: tags,
|
||||
CreatedAt: f.CreatedAt.String(),
|
||||
UpdatedAt: f.UpdatedAt.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func saveUploadedFile(file *multipart.FileHeader, dst string) error {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, readErr := src.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -10,21 +10,21 @@ import (
|
||||
)
|
||||
|
||||
type OCRJob struct {
|
||||
FileID string
|
||||
FilePath string
|
||||
ResourceID string
|
||||
FilePath string
|
||||
}
|
||||
|
||||
type OCRService struct {
|
||||
client *ocr.Client
|
||||
fileSvc *FileService
|
||||
jobs chan OCRJob
|
||||
client *ocr.Client
|
||||
resourceSvc *ResourceService
|
||||
jobs chan OCRJob
|
||||
}
|
||||
|
||||
func NewOCRService(cfg *config.Config, fileSvc *FileService) *OCRService {
|
||||
func NewOCRService(cfg *config.Config, resourceSvc *ResourceService) *OCRService {
|
||||
return &OCRService{
|
||||
client: ocr.NewClient(cfg.OCREndpoint),
|
||||
fileSvc: fileSvc,
|
||||
jobs: make(chan OCRJob, 100),
|
||||
client: ocr.NewClient(cfg.OCREndpoint),
|
||||
resourceSvc: resourceSvc,
|
||||
jobs: make(chan OCRJob, 100),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ func (s *OCRService) Stop() {
|
||||
log.Println("[OCR] Worker stopped")
|
||||
}
|
||||
|
||||
func (s *OCRService) Enqueue(fileID, filePath string) {
|
||||
s.jobs <- OCRJob{FileID: fileID, FilePath: filePath}
|
||||
log.Printf("[OCR] Enqueued file %s", fileID)
|
||||
func (s *OCRService) Enqueue(resourceID, filePath string) {
|
||||
s.jobs <- OCRJob{ResourceID: resourceID, FilePath: filePath}
|
||||
log.Printf("[OCR] Enqueued resource %s", resourceID)
|
||||
}
|
||||
|
||||
func (s *OCRService) worker() {
|
||||
@@ -50,28 +50,28 @@ func (s *OCRService) worker() {
|
||||
}
|
||||
|
||||
func (s *OCRService) process(job OCRJob) {
|
||||
log.Printf("[OCR] Processing file %s", job.FileID)
|
||||
log.Printf("[OCR] Processing resource %s", job.ResourceID)
|
||||
|
||||
data, err := os.ReadFile(job.FilePath)
|
||||
if err != nil {
|
||||
log.Printf("[OCR] Failed to read file %s: %v", job.FileID, err)
|
||||
log.Printf("[OCR] Failed to read resource %s: %v", job.ResourceID, err)
|
||||
return
|
||||
}
|
||||
|
||||
blocks, err := s.client.Recognize(data)
|
||||
if err != nil {
|
||||
log.Printf("[OCR] Failed to recognize file %s: %v", job.FileID, err)
|
||||
log.Printf("[OCR] Failed to recognize resource %s: %v", job.ResourceID, err)
|
||||
return
|
||||
}
|
||||
|
||||
text := s.FlattenResults(blocks)
|
||||
|
||||
if err := s.fileSvc.UpdateOCRText(job.FileID, text); err != nil {
|
||||
log.Printf("[OCR] Failed to update ocr_text for file %s: %v", job.FileID, err)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[OCR] Completed file %s (%d chars)", job.FileID, len(text))
|
||||
log.Printf("[OCR] Completed resource %s (%d chars)", job.ResourceID, len(text))
|
||||
}
|
||||
|
||||
func (s *OCRService) RecognizeFromBytes(data []byte) ([]ocr.TextBlock, error) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *PlacementService) ensureServerLocation(userID uuid.UUID) (db.StorageLocation, error) {
|
||||
loc, err := s.queries.GetServerStorageLocation(context.Background(), userID)
|
||||
if err != nil {
|
||||
return s.queries.CreateStorageLocation(context.Background(), db.CreateStorageLocationParams{
|
||||
UserID: userID,
|
||||
DeviceName: "VaultDrop Server",
|
||||
Role: "server",
|
||||
})
|
||||
}
|
||||
return loc, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
type ResourceService struct {
|
||||
queries *db.Queries
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewResourceService(queries *db.Queries, cfg *config.Config) *ResourceService {
|
||||
return &ResourceService{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)
|
||||
}
|
||||
|
||||
if err := saveUploadedFile(file, dst); err != nil {
|
||||
return nil, fmt.Errorf("save file: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read saved file: %w", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat file: %w", err)
|
||||
}
|
||||
|
||||
checksum := hex.EncodeToString(CreateSHA256Hash(data))
|
||||
|
||||
ownerUUID, err := uuid.Parse(ownerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse owner id: %w", err)
|
||||
}
|
||||
|
||||
existing, err := s.queries.FindDuplicateByChecksum(context.Background(), db.FindDuplicateByChecksumParams{
|
||||
Checksum: checksum,
|
||||
OwnerID: ownerUUID,
|
||||
})
|
||||
if err == nil && existing.ID != uuid.Nil {
|
||||
os.Remove(dst)
|
||||
placement, err := s.queries.GetServerPlacementByResource(context.Background(), existing.ID)
|
||||
if err == nil {
|
||||
return &model.UploadResult{
|
||||
ID: existing.ID.String(),
|
||||
Name: existing.Name,
|
||||
Path: placement.StorageKey.String,
|
||||
MimeType: existing.MimeType,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
dbResource, err := s.queries.CreateResource(context.Background(), db.CreateResourceParams{
|
||||
Name: file.Filename,
|
||||
MimeType: file.Header.Get("Content-Type"),
|
||||
Size: info.Size(),
|
||||
Checksum: checksum,
|
||||
OwnerID: ownerUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create resource in db: %w", err)
|
||||
}
|
||||
|
||||
placement, err := s.ensureServerPlacement(dbResource.ID, dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create server placement: %w", err)
|
||||
}
|
||||
|
||||
if err := s.ensureOwnerRebac(dbResource.ID, ownerUUID); err != nil {
|
||||
return nil, fmt.Errorf("create owner rebac: %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 uuid.UUID, dst string) (db.ResourcePlacement, error) {
|
||||
serverLoc, err := s.queries.GetServerStorageLocation(context.Background(), uuid.Nil)
|
||||
if err != nil {
|
||||
return db.ResourcePlacement{}, fmt.Errorf("get server location: %w", err)
|
||||
}
|
||||
|
||||
placement, err := s.queries.CreatePlacement(context.Background(), 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) ensureOwnerRebac(resourceID, ownerID uuid.UUID) error {
|
||||
_, err := s.queries.CreateRebacRelation(context.Background(), db.CreateRebacRelationParams{
|
||||
ResourceID: resourceID,
|
||||
SubjectUserID: ownerID,
|
||||
Role: "owner",
|
||||
GrantedBy: ownerID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ResourceService) List(ownerID string) ([]model.Resource, error) {
|
||||
ownerUUID, _ := uuid.Parse(ownerID)
|
||||
dbResources, err := s.queries.ListResourcesByOwner(context.Background(), ownerUUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list resources: %w", err)
|
||||
}
|
||||
|
||||
resources := make([]model.Resource, len(dbResources))
|
||||
for i, r := range dbResources {
|
||||
tags, err := s.queries.GetTagsByResourceID(context.Background(), sql.NullString{String: r.ID.String(), Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get tags for resource %s: %w", r.ID, err)
|
||||
}
|
||||
resources[i] = dbResourceToModel(r, tags)
|
||||
}
|
||||
return resources, 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(), sql.NullString{String: r.ID.String(), Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get tags: %w", err)
|
||||
}
|
||||
m := dbResourceToModel(r, tags)
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (s *ResourceService) Delete(id string) error {
|
||||
resourceUUID, _ := uuid.Parse(id)
|
||||
return s.queries.DeleteResource(context.Background(), resourceUUID)
|
||||
}
|
||||
|
||||
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: sql.NullString{String: tag.ID, Valid: true},
|
||||
ResourceID: sql.NullString{String: resourceUUID.String(), Valid: true},
|
||||
})
|
||||
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(), sql.NullString{String: resourceUUID.String(), Valid: true})
|
||||
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, 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) (*model.Resource, error) {
|
||||
ownerUUID, _ := uuid.Parse(ownerID)
|
||||
r, err := s.queries.CreateFolder(context.Background(), db.CreateFolderParams{
|
||||
Name: name,
|
||||
OwnerID: ownerUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create folder: %w", err)
|
||||
}
|
||||
|
||||
if err := s.ensureOwnerRebac(r.ID, ownerUUID); err != nil {
|
||||
return nil, fmt.Errorf("create owner rebac: %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) ([]model.Resource, error) {
|
||||
parentUUID, _ := uuid.Parse(parentID)
|
||||
ownerUUID, _ := uuid.Parse(ownerID)
|
||||
dbResources, err := s.queries.ListResourcesByParentAndOwner(context.Background(), db.ListResourcesByParentAndOwnerParams{
|
||||
ParentResourceID: uuid.NullUUID{UUID: parentUUID, Valid: true},
|
||||
OwnerID: ownerUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 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(), sql.NullString{String: r.ID.String(), Valid: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get tags for resource %s: %w", r.ID, err)
|
||||
}
|
||||
resources[i] = dbResourceToModel(r, tags)
|
||||
}
|
||||
return resources, 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, 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,
|
||||
Checksum: r.Checksum,
|
||||
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) error {
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, readErr := src.Read(buf)
|
||||
if n > 0 {
|
||||
if _, writeErr := out.Write(buf[:n]); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -25,43 +25,43 @@ func NewURLService(secret, serverHost string, expiryMinutes int) *URLService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *URLService) sign(fileID string, expires int64) string {
|
||||
data := fmt.Sprintf("%s:%d", fileID, expires)
|
||||
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(fileUUID string) string {
|
||||
func (s *URLService) GenerateDownloadURL(resourceUUID string) string {
|
||||
expires := time.Now().Add(s.expiryDuration).Unix()
|
||||
sig := s.sign(fileUUID, expires)
|
||||
sig := s.sign(resourceUUID, expires)
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s/api/v1/files/download/%s?expires=%d&sig=%s",
|
||||
"%s/api/v1/resources/download/%s?expires=%d&sig=%s",
|
||||
s.serverHost,
|
||||
fileUUID,
|
||||
resourceUUID,
|
||||
expires,
|
||||
sig,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *URLService) GenerateThumbnailURL(thumbUUID string) string {
|
||||
func (s *URLService) GenerateVariantURL(variantUUID string) string {
|
||||
expires := time.Now().Add(s.expiryDuration).Unix()
|
||||
sig := s.sign(thumbUUID, expires)
|
||||
sig := s.sign(variantUUID, expires)
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s/api/v1/thumbnails/%s?expires=%d&sig=%s",
|
||||
"%s/api/v1/variants/%s?expires=%d&sig=%s",
|
||||
s.serverHost,
|
||||
thumbUUID,
|
||||
variantUUID,
|
||||
expires,
|
||||
sig,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *URLService) Validate(fileID, sig string, expires int64) bool {
|
||||
func (s *URLService) Validate(id, sig string, expires int64) bool {
|
||||
if time.Now().Unix() > expires {
|
||||
return false
|
||||
}
|
||||
expected := s.sign(fileID, expires)
|
||||
expected := s.sign(id, expires)
|
||||
return hmac.Equal([]byte(sig), []byte(expected))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user