세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.7 KiB
SQL
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;
|