Files
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

237 lines
10 KiB
SQL

-- =====================================================================
-- FILE_MGR SCHEMA — File Manager, Storage Quotas, Cleanup Scheduler
-- =====================================================================
SET search_path TO file_mgr, public;
CREATE TYPE file_kind AS ENUM ('Video','Image','Audio','Voiceover','Document','Other');
CREATE TYPE folder_kind AS ENUM ('System','User','Shared','Tenant');
CREATE TYPE upload_status AS ENUM ('Pending','Uploading','Processing','Ready','Failed','Quarantined');
CREATE TYPE cleanup_entity_type AS ENUM ('Export','TempRenderFolder','OrphanedFile','UnusedUpload','SnapshotExpired');
CREATE TYPE cleanup_status AS ENUM ('Scheduled','Notified','Processing','Done','Skipped','Failed');
-- ---------------------------------------------------------------------
-- user_folders — hierarchical folders
-- ---------------------------------------------------------------------
CREATE TABLE user_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
name TEXT NOT NULL,
folder_type folder_kind NOT NULL DEFAULT 'User',
parent_folder_id UUID REFERENCES user_folders(id) ON DELETE CASCADE,
-- Stats (denormalized for fast UI)
file_count INT NOT NULL DEFAULT 0,
total_size_bytes BIGINT NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
is_shared BOOLEAN NOT NULL DEFAULT FALSE,
share_token TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_folders_user ON user_folders(user_id, parent_folder_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_folders_parent ON user_folders(parent_folder_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_folders_share ON user_folders(share_token) WHERE share_token IS NOT NULL;
CREATE TRIGGER tg_folders_updated_at
BEFORE UPDATE ON user_folders FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- user_files
-- ---------------------------------------------------------------------
CREATE TABLE user_files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
user_folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL,
-- Identity
name TEXT NOT NULL,
original_filename TEXT,
file_extension TEXT,
mime_type TEXT,
file_type file_kind NOT NULL,
-- Storage
minio_bucket TEXT NOT NULL,
minio_key TEXT NOT NULL,
cdn_url TEXT,
file_address TEXT NOT NULL, -- canonical URL
size_bytes BIGINT NOT NULL,
md5_hash TEXT,
sha256_hash TEXT,
-- Media metadata
duration_sec NUMERIC(8,2),
width INT,
height INT,
fps NUMERIC(5,2),
bitrate_kbps INT,
codec TEXT,
has_audio BOOLEAN,
has_video BOOLEAN,
-- Thumbnails
thumbnail_url TEXT,
waveform_data JSONB, -- for audio files
-- Upload state
upload_status upload_status NOT NULL DEFAULT 'Ready',
upload_id TEXT, -- multipart upload ID if used
upload_progress INT NOT NULL DEFAULT 100,
processing_error TEXT,
-- Source / linkage
source TEXT, -- 'upload','export','snapshot','voiceover_record','stock'
export_id UUID, -- references render.exports
parent_file_id UUID REFERENCES user_files(id) ON DELETE SET NULL, -- derived files
-- Lifecycle
last_used_at TIMESTAMPTZ,
use_count INT NOT NULL DEFAULT 0,
-- Sharing
is_public BOOLEAN NOT NULL DEFAULT FALSE,
share_token TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_files_user_folder ON user_files(user_id, user_folder_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_tenant ON user_files(tenant_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_type ON user_files(user_id, file_type) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_hash ON user_files(md5_hash) WHERE md5_hash IS NOT NULL;
CREATE INDEX idx_files_unused ON user_files(last_used_at) WHERE deleted_at IS NULL;
CREATE INDEX idx_files_name_trgm ON user_files USING gin (name gin_trgm_ops);
CREATE INDEX idx_files_share ON user_files(share_token) WHERE share_token IS NOT NULL;
CREATE TRIGGER tg_files_updated_at
BEFORE UPDATE ON user_files FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- storage_quotas — current usage per user
-- ---------------------------------------------------------------------
CREATE TABLE storage_quotas (
user_id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
plan_quota_bytes BIGINT NOT NULL DEFAULT 0, -- from plan
bonus_quota_bytes BIGINT NOT NULL DEFAULT 0, -- purchased extra
used_bytes BIGINT NOT NULL DEFAULT 0,
-- Cached counts
video_count INT NOT NULL DEFAULT 0,
image_count INT NOT NULL DEFAULT 0,
audio_count INT NOT NULL DEFAULT 0,
video_bytes BIGINT NOT NULL DEFAULT 0,
image_bytes BIGINT NOT NULL DEFAULT 0,
audio_bytes BIGINT NOT NULL DEFAULT 0,
-- Notifications
last_90pct_notified_at TIMESTAMPTZ,
last_100pct_notified_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quotas_tenant ON storage_quotas(tenant_id);
CREATE TRIGGER tg_quotas_updated_at
BEFORE UPDATE ON storage_quotas FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- cleanup_schedules — track what's queued for auto-delete
-- ---------------------------------------------------------------------
CREATE TABLE cleanup_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID,
user_id UUID,
entity_type cleanup_entity_type NOT NULL,
entity_id UUID NOT NULL,
entity_path TEXT, -- filesystem path for temp folders
scheduled_delete_at TIMESTAMPTZ NOT NULL,
notify_user_at TIMESTAMPTZ, -- send "expires in 3 days" notice
user_notified BOOLEAN NOT NULL DEFAULT FALSE,
user_notified_at TIMESTAMPTZ,
status cleanup_status NOT NULL DEFAULT 'Scheduled',
processed_at TIMESTAMPTZ,
processing_error TEXT,
bytes_freed BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_cleanup_due ON cleanup_schedules(scheduled_delete_at) WHERE status = 'Scheduled';
CREATE INDEX idx_cleanup_notify_due ON cleanup_schedules(notify_user_at) WHERE user_notified = FALSE AND notify_user_at IS NOT NULL;
CREATE INDEX idx_cleanup_entity ON cleanup_schedules(entity_type, entity_id);
CREATE TRIGGER tg_cleanup_updated_at
BEFORE UPDATE ON cleanup_schedules FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- upload_sessions — multipart / chunked upload tracking
-- ---------------------------------------------------------------------
CREATE TABLE upload_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
minio_bucket TEXT NOT NULL,
minio_key TEXT NOT NULL,
minio_upload_id TEXT NOT NULL, -- S3/MinIO multipart ID
filename TEXT NOT NULL,
mime_type TEXT,
total_size_bytes BIGINT NOT NULL,
chunks_received INT NOT NULL DEFAULT 0,
bytes_received BIGINT NOT NULL DEFAULT 0,
chunk_size_bytes INT NOT NULL DEFAULT 5242880, -- 5MB default
target_folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL,
target_file_id UUID, -- created when complete
status upload_status NOT NULL DEFAULT 'Uploading',
error_message TEXT,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_uploads_user ON upload_sessions(user_id, created_at DESC);
CREATE INDEX idx_uploads_expired ON upload_sessions(expires_at) WHERE status = 'Uploading';
CREATE TRIGGER tg_uploads_updated_at
BEFORE UPDATE ON upload_sessions FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- minio_buckets — bucket registry (per region, per purpose)
-- ---------------------------------------------------------------------
CREATE TABLE minio_buckets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
region TEXT NOT NULL,
endpoint TEXT NOT NULL,
purpose TEXT NOT NULL, -- 'templates','user-uploads','exports','snapshots','voiceovers'
is_public BOOLEAN NOT NULL DEFAULT FALSE,
cdn_base_url TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_buckets_region_purpose ON minio_buckets(region, purpose) WHERE is_active = TRUE;