Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue (SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived keys and a fully private bucket, OIDC photographer login with per-request allowlist checks, client share links with argon2 passwords and lockout, cookie-based image authorization with sliding expiry, hand-rolled spec-compliant streaming ZIP downloads with exact Content-Length, React + Vite gallery frontend, single Docker image, Helm chart for external S3 + Postgres, and Gitea CI. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
create table users (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
oidc_subject text not null unique,
|
||||
email text not null,
|
||||
display_name text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table albums (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text not null default '',
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table photos (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
album_id uuid not null references albums(id) on delete cascade,
|
||||
filename text not null,
|
||||
content_type text not null,
|
||||
size_bytes bigint not null default 0,
|
||||
status text not null default 'uploaded', -- uploaded | processing | ready | error
|
||||
error text,
|
||||
width int,
|
||||
height int,
|
||||
taken_at timestamptz,
|
||||
-- Bumped on every (re)process; cache-buster for preview/thumb URLs.
|
||||
processed_at timestamptz,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index photos_album_idx on photos (album_id);
|
||||
|
||||
create table shares (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
album_id uuid not null references albums(id) on delete cascade,
|
||||
token text not null unique,
|
||||
label text not null default '',
|
||||
password_hash text,
|
||||
allow_download boolean not null default true,
|
||||
expires_at timestamptz,
|
||||
-- Brute-force lockout state for password-protected shares.
|
||||
failed_attempts int not null default 0,
|
||||
locked_until timestamptz,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index shares_album_idx on shares (album_id);
|
||||
|
||||
create table ratings (
|
||||
share_id uuid not null references shares(id) on delete cascade,
|
||||
photo_id uuid not null references photos(id) on delete cascade,
|
||||
rating int not null check (rating between 1 and 5),
|
||||
updated_at timestamptz not null default now(),
|
||||
primary key (share_id, photo_id)
|
||||
);
|
||||
|
||||
-- Cascaded photo deletes fire per-row FK triggers; without this each one
|
||||
-- sequential-scans the table.
|
||||
create index ratings_photo_idx on ratings (photo_id);
|
||||
|
||||
create table tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
share_id uuid not null references shares(id) on delete cascade,
|
||||
photo_id uuid not null references photos(id) on delete cascade,
|
||||
tag text not null,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (share_id, photo_id, tag)
|
||||
);
|
||||
|
||||
create index tags_photo_idx on tags (photo_id);
|
||||
|
||||
create table jobs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
kind text not null,
|
||||
payload jsonb not null default '{}',
|
||||
status text not null default 'queued', -- queued | running | done | failed
|
||||
attempts int not null default 0,
|
||||
max_attempts int not null default 5,
|
||||
run_at timestamptz not null default now(),
|
||||
locked_by text,
|
||||
locked_at timestamptz,
|
||||
last_error text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index jobs_poll_idx on jobs (status, run_at);
|
||||
Reference in New Issue
Block a user