From eda7a0ee96da5666cb1ab67d116627ad52c62d53 Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Wed, 12 Aug 2026 16:04:31 -0400 Subject: [PATCH] feat(share): extend the share table into an index of active shares --- .../database/SqliteDatabaseClient.test.ts | 2 +- .../clients/database/SqliteDatabaseClient.ts | 1 + .../migrations/mysql/mysql_mig_20.sql | 89 +++++++++++++++++++ .../migrations/postgres/postgres_mig_9.sql | 34 +++++++ .../migrations/sqlite/0065_share_entries.sql | 51 +++++++++++ 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/backend/clients/database/migrations/mysql/mysql_mig_20.sql create mode 100644 src/backend/clients/database/migrations/postgres/postgres_mig_9.sql create mode 100644 src/backend/clients/database/migrations/sqlite/0065_share_entries.sql diff --git a/src/backend/clients/database/SqliteDatabaseClient.test.ts b/src/backend/clients/database/SqliteDatabaseClient.test.ts index c378b3548..c7000bb73 100644 --- a/src/backend/clients/database/SqliteDatabaseClient.test.ts +++ b/src/backend/clients/database/SqliteDatabaseClient.test.ts @@ -27,7 +27,7 @@ import { DatabaseClientFactory } from './index.js'; import { SqliteDatabaseClient } from './SqliteDatabaseClient.js'; /** Highest schema version the migration table can reach. */ -const CURRENT_SCHEMA_VERSION = 60; +const CURRENT_SCHEMA_VERSION = 61; const SYSTEM_USER_UUID = '5d4adce0-a381-4982-9c02-6e2540026238'; const sqliteConfig = ( diff --git a/src/backend/clients/database/SqliteDatabaseClient.ts b/src/backend/clients/database/SqliteDatabaseClient.ts index df684c743..95a407850 100644 --- a/src/backend/clients/database/SqliteDatabaseClient.ts +++ b/src/backend/clients/database/SqliteDatabaseClient.ts @@ -94,6 +94,7 @@ const AVAILABLE_MIGRATIONS: [number, string[]][] = [ [57, ['0062_blocked-app-origins.sql']], [58, ['0063_add_suspended_reason.sql']], [59, ['0064_abuse-moderation-events.sql']], + [60, ['0065_share_entries.sql']], ]; export class SqliteDatabaseClient extends AbstractDatabaseClient { diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_20.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_20.sql new file mode 100644 index 000000000..60aebb780 --- /dev/null +++ b/src/backend/clients/database/migrations/mysql/mysql_mig_20.sql @@ -0,0 +1,89 @@ +-- Copyright (C) 2024-present Puter Technologies Inc. +-- +-- This file is part of Puter. +-- +-- Puter is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published +-- by the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +-- Grow `share` from a pending-email-invite table into the index of active +-- shares. See sqlite/0065_share_entries.sql for the column rationale. +-- +-- Idempotent: columns go through _puter_add_col (from mig_1), indexes and +-- foreign keys through the guarded procedure below. There is no per-file +-- applied-state tracking, so every statement must tolerate a re-run. + +CALL _puter_add_col('share', 'holder_user_id', '`holder_user_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('share', 'fsentry_id', '`fsentry_id` int unsigned DEFAULT NULL'); +CALL _puter_add_col('share', 'mode', '`mode` varchar(20) DEFAULT NULL'); +CALL _puter_add_col('share', 'applied_at', '`applied_at` timestamp NULL DEFAULT NULL'); + +DROP PROCEDURE IF EXISTS _puter_add_share_index_constraints; +DELIMITER // +CREATE PROCEDURE _puter_add_share_index_constraints() +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'share' + AND INDEX_NAME = 'idx_share_holder' + ) THEN + ALTER TABLE `share` ADD INDEX `idx_share_holder` (`holder_user_id`, `id`); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'share' + AND INDEX_NAME = 'idx_share_fsentry' + ) THEN + ALTER TABLE `share` ADD INDEX `idx_share_fsentry` (`fsentry_id`); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'share' + AND INDEX_NAME = 'idx_share_holder_entry_issuer' + ) THEN + ALTER TABLE `share` ADD UNIQUE INDEX `idx_share_holder_entry_issuer` + (`holder_user_id`, `fsentry_id`, `issuer_user_id`); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'share' + AND CONSTRAINT_NAME = 'share_holder_user_fk' + ) THEN + ALTER TABLE `share` ADD CONSTRAINT `share_holder_user_fk` + FOREIGN KEY (`holder_user_id`) REFERENCES `user` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE; + END IF; + + -- The cascade that retires a share with its file. + IF NOT EXISTS ( + SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'share' + AND CONSTRAINT_NAME = 'share_fsentry_fk' + ) THEN + ALTER TABLE `share` ADD CONSTRAINT `share_fsentry_fk` + FOREIGN KEY (`fsentry_id`) REFERENCES `fsentries` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE; + END IF; +END// +DELIMITER ; + +CALL _puter_add_share_index_constraints(); + +DROP PROCEDURE IF EXISTS _puter_add_share_index_constraints; diff --git a/src/backend/clients/database/migrations/postgres/postgres_mig_9.sql b/src/backend/clients/database/migrations/postgres/postgres_mig_9.sql new file mode 100644 index 000000000..5edce7d05 --- /dev/null +++ b/src/backend/clients/database/migrations/postgres/postgres_mig_9.sql @@ -0,0 +1,34 @@ +-- Copyright (C) 2024-present Puter Technologies Inc. +-- +-- This file is part of Puter. +-- +-- Puter is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published +-- by the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +-- Grow share from a pending-email-invite table into the index of active +-- shares. See sqlite/0065_share_entries.sql for the column rationale. + +ALTER TABLE share + ADD COLUMN IF NOT EXISTS holder_user_id integer + REFERENCES "user" (id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD COLUMN IF NOT EXISTS fsentry_id integer + REFERENCES fsentries (id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD COLUMN IF NOT EXISTS mode varchar(20), + ADD COLUMN IF NOT EXISTS applied_at timestamp; + +CREATE INDEX IF NOT EXISTS idx_share_holder + ON share (holder_user_id, id); +CREATE INDEX IF NOT EXISTS idx_share_fsentry + ON share (fsentry_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_share_holder_entry_issuer + ON share (holder_user_id, fsentry_id, issuer_user_id); diff --git a/src/backend/clients/database/migrations/sqlite/0065_share_entries.sql b/src/backend/clients/database/migrations/sqlite/0065_share_entries.sql new file mode 100644 index 000000000..40941a198 --- /dev/null +++ b/src/backend/clients/database/migrations/sqlite/0065_share_entries.sql @@ -0,0 +1,51 @@ +-- Copyright (C) 2024-present Puter Technologies Inc. +-- +-- This file is part of Puter. +-- +-- Puter is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published +-- by the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +-- Grow `share` from a pending-email-invite table into the index of active +-- shares. Permissions stay the source of truth for access; this is what makes +-- shares listable and gives them a lifecycle. +-- +-- - `holder_user_id` : NULL while an invite awaits signup. +-- - `fsentry_id` : the shared node. ON DELETE CASCADE retires the share +-- with the file, which permissions alone don't do. +-- - `mode` : see|list|read|write|manage. Unconstrained on purpose — +-- ACLService owns the mode set and adding one shouldn't +-- need a three-dialect migration. +-- - `applied_at` : set when an invite is claimed. Claiming updates the +-- row rather than deleting it, so the share stays +-- queryable. + +ALTER TABLE `share` ADD COLUMN `holder_user_id` INTEGER DEFAULT NULL + REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE `share` ADD COLUMN `fsentry_id` INTEGER DEFAULT NULL + REFERENCES `fsentries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE `share` ADD COLUMN `mode` TEXT DEFAULT NULL; +ALTER TABLE `share` ADD COLUMN `applied_at` TIMESTAMP DEFAULT NULL; + +-- "Shared with me", keyset-paginated: ORDER BY ends in `id` as the tiebreaker. +CREATE INDEX IF NOT EXISTS `idx_share_holder` + ON `share` (`holder_user_id`, `id`); + +-- Who has access to one node — also how an owner sees a manage-delegate's +-- re-grants, which the issuer/holder permission tables can't answer. +CREATE INDEX IF NOT EXISTS `idx_share_fsentry` ON `share` (`fsentry_id`); + +-- One row per (holder, node, issuer). Pending invites have a NULL +-- holder_user_id and so aren't covered here; dedup for those belongs with the +-- invite flow. +CREATE UNIQUE INDEX IF NOT EXISTS `idx_share_holder_entry_issuer` + ON `share` (`holder_user_id`, `fsentry_id`, `issuer_user_id`);