Files
sori.studio/db/migrations/069_media_catalog.sql
zenn de767095ba v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-22 11:05:40 +09:00

149 lines
4.7 KiB
SQL

ALTER TABLE media_metadata
ADD COLUMN IF NOT EXISTS id BIGINT GENERATED BY DEFAULT AS IDENTITY,
ADD COLUMN IF NOT EXISTS file_name TEXT,
ADD COLUMN IF NOT EXISTS storage_key TEXT,
ADD COLUMN IF NOT EXISTS extension TEXT,
ADD COLUMN IF NOT EXISTS kind TEXT,
ADD COLUMN IF NOT EXISTS role TEXT,
ADD COLUMN IF NOT EXISTS size_bytes BIGINT,
ADD COLUMN IF NOT EXISTS modified_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS parent_id BIGINT,
ADD COLUMN IF NOT EXISTS origin TEXT,
ADD COLUMN IF NOT EXISTS status TEXT,
ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS missing_since TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS error_detail TEXT;
UPDATE media_metadata
SET
file_name = COALESCE(file_name, regexp_replace(url, '^.*/', '')),
storage_key = COALESCE(storage_key, regexp_replace(url, '^/uploads/', '')),
extension = COALESCE(extension, lower(substring(url from '\.[^.?#/]+$'))),
kind = COALESCE(kind, 'file'),
role = COALESCE(role, 'original'),
origin = COALESCE(origin, 'external'),
status = COALESCE(status, 'staging')
WHERE file_name IS NULL
OR storage_key IS NULL
OR kind IS NULL
OR role IS NULL
OR origin IS NULL
OR status IS NULL;
ALTER TABLE media_metadata
ALTER COLUMN file_name SET NOT NULL,
ALTER COLUMN storage_key SET NOT NULL,
ALTER COLUMN kind SET NOT NULL,
ALTER COLUMN role SET NOT NULL,
ALTER COLUMN origin SET NOT NULL,
ALTER COLUMN status SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS media_metadata_id_unique_idx
ON media_metadata (id);
CREATE UNIQUE INDEX IF NOT EXISTS media_metadata_storage_key_unique_idx
ON media_metadata (storage_key);
CREATE INDEX IF NOT EXISTS media_metadata_status_updated_idx
ON media_metadata (status, modified_at DESC);
CREATE INDEX IF NOT EXISTS media_metadata_kind_role_idx
ON media_metadata (kind, role);
CREATE INDEX IF NOT EXISTS media_metadata_parent_idx
ON media_metadata (parent_id)
WHERE parent_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS media_metadata_last_seen_idx
ON media_metadata (last_seen_at DESC);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'media_metadata_kind_check'
) THEN
ALTER TABLE media_metadata
ADD CONSTRAINT media_metadata_kind_check
CHECK (kind IN ('image', 'video', 'audio', 'file'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'media_metadata_status_check'
) THEN
ALTER TABLE media_metadata
ADD CONSTRAINT media_metadata_status_check
CHECK (status IN ('staging', 'active', 'missing', 'deleting', 'error'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'media_metadata_origin_check'
) THEN
ALTER TABLE media_metadata
ADD CONSTRAINT media_metadata_origin_check
CHECK (origin IN ('upload', 'import', 'external', 'derived', 'system'));
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'media_metadata_parent_fk'
) THEN
ALTER TABLE media_metadata
ADD CONSTRAINT media_metadata_parent_fk
FOREIGN KEY (parent_id)
REFERENCES media_metadata (id)
ON DELETE SET NULL;
END IF;
END
$$;
CREATE TABLE IF NOT EXISTS media_references (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
media_id BIGINT REFERENCES media_metadata (id) ON DELETE CASCADE,
media_url TEXT NOT NULL,
reference_type TEXT NOT NULL,
reference_id TEXT NOT NULL,
reference_field TEXT NOT NULL,
reference_title TEXT NOT NULL DEFAULT '',
reference_slug TEXT NOT NULL DEFAULT '',
reference_status TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT media_references_target_unique
UNIQUE (media_url, reference_type, reference_id, reference_field)
);
CREATE INDEX IF NOT EXISTS media_references_media_id_idx
ON media_references (media_id)
WHERE media_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS media_references_url_idx
ON media_references (media_url);
CREATE INDEX IF NOT EXISTS media_references_target_idx
ON media_references (reference_type, reference_id);
CREATE TABLE IF NOT EXISTS media_catalog_state (
singleton BOOLEAN PRIMARY KEY DEFAULT true,
ready BOOLEAN NOT NULL DEFAULT false,
reconciled_at TIMESTAMPTZ,
filesystem_count BIGINT NOT NULL DEFAULT 0,
filesystem_bytes BIGINT NOT NULL DEFAULT 0,
catalog_count BIGINT NOT NULL DEFAULT 0,
catalog_bytes BIGINT NOT NULL DEFAULT 0,
error_detail TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT media_catalog_state_singleton_check
CHECK (singleton = true)
);
INSERT INTO media_catalog_state (singleton, ready)
VALUES (true, false)
ON CONFLICT (singleton) DO NOTHING;