feat: convert file manager into a plugin

This commit is contained in:
LukeGus committed 2026-09-23 16:27:29 -05:00
1 parent b76da9ab52
commit c85c1fdb86
259 files changed
+54155 -16791

No files matched your search

@@ -0,0 +1,100 @@
/**
* Moves the three file-manager host columns into the file-manager plugin's
* host-scope settings, before those columns are dropped from ssh_data.
*
* Idempotent: skips a host that already has a file-manager settings row, so
* it is safe to run on every boot. Lossless: reads straight off the
* still-present ssh_data columns, so it must run and finish before those
* columns are ever dropped from schema.ts.
*/
import { sql } from "drizzle-orm";
import { databaseLogger } from "../logger.js";
import { getDb } from "../../database/db/index.js";
import { createCurrentPluginSettingsRepository } from "../../database/repositories/factory.js";
export interface FileManagerSettingsMigrationResult {
moved: number;
skipped: number;
}
interface LegacyFileManagerRow {
id: number;
enable_file_manager: number | boolean | null;
default_path: string | null;
scp_legacy: number | boolean | null;
}
export async function runFileManagerSettingsMigration(): Promise<FileManagerSettingsMigrationResult> {
const result: FileManagerSettingsMigrationResult = { moved: 0, skipped: 0 };
try {
const drizzleDb = getDb();
// Raw SQL, not the typed schema: this runs once schema.ts has already
// dropped these columns, so the query is the only thing left that still
// knows they used to exist.
const rows = await drizzleDb.all<LegacyFileManagerRow>(sql`
SELECT id, enable_file_manager, default_path, scp_legacy
FROM ssh_data
WHERE enable_file_manager = false OR default_path IS NOT NULL
OR scp_legacy = true
`);
if (rows.length === 0) return result;
const pluginSettingsRepository = createCurrentPluginSettingsRepository();
for (const row of rows) {
const scopeId = String(row.id);
const existing = await pluginSettingsRepository.get(
"file-manager",
"host",
scopeId,
"enableFileManager",
);
if (existing && existing.value !== null) {
result.skipped++;
continue;
}
await pluginSettingsRepository.set(
"file-manager",
"host",
scopeId,
"enableFileManager",
JSON.stringify(row.enable_file_manager !== false),
);
await pluginSettingsRepository.set(
"file-manager",
"host",
scopeId,
"defaultPath",
JSON.stringify(row.default_path ?? null),
);
await pluginSettingsRepository.set(
"file-manager",
"host",
scopeId,
"scpLegacy",
JSON.stringify(!!row.scp_legacy),
);
result.moved++;
}
if (result.moved > 0) {
databaseLogger.info(
`Moved file manager settings for ${result.moved} host(s) into plugin settings`,
{ operation: "file_manager_settings_migration", moved: result.moved },
);
}
} catch (error) {
// A failed migration must not stop the backend. Hosts simply keep
// whatever file-manager settings they already had (none, on a fresh run).
databaseLogger.warn("File manager settings migration failed", {
operation: "file_manager_settings_migration",
error: error instanceof Error ? error.message : String(error),
});
}
return result;
}
+30 -15
View File
@@ -1,13 +1,35 @@
import { sql } from "drizzle-orm";
import {
createCurrentDismissedAlertRepository,
createCurrentFileManagerBookmarkRepository,
createCurrentTransferRecentRepository,
createCurrentUserDataExportRepository,
createCurrentUserRepository,
} from "../database/repositories/factory.js";
import { getDb } from "../database/db/index.js";
import { DataCrypto } from "./data-crypto.js";
import { databaseLogger } from "./logger.js";
// The file manager plugin owns these tables now (p_file_manager_*); read by
// raw SQL rather than importing plugin code into core.
async function listFileManagerExportData(userId: string): Promise<{
recent: unknown[];
pinned: unknown[];
shortcuts: unknown[];
transferRecent: unknown[];
}> {
const db = getDb();
const [recent, pinned, shortcuts, transferRecent] = await Promise.all([
db.all(sql`SELECT * FROM p_file_manager_recent WHERE user_id = ${userId}`),
db.all(sql`SELECT * FROM p_file_manager_pinned WHERE user_id = ${userId}`),
db.all(
sql`SELECT * FROM p_file_manager_shortcuts WHERE user_id = ${userId}`,
),
db.all(
sql`SELECT * FROM p_file_manager_transfer_recent WHERE user_id = ${userId}`,
),
]);
return { recent, pinned, shortcuts, transferRecent };
}
interface UserExportData {
version: string;
exportedAt: string;
@@ -90,19 +112,12 @@ class UserDataExport {
: credentials;
}
const [recentFiles, pinnedFiles, shortcuts, transferRecentData] =
await Promise.all([
createCurrentFileManagerBookmarkRepository().listRecentByUserId(
userId,
),
createCurrentFileManagerBookmarkRepository().listPinnedByUserId(
userId,
),
createCurrentFileManagerBookmarkRepository().listShortcutsByUserId(
userId,
),
createCurrentTransferRecentRepository().listByUserId(userId),
]);
const {
recent: recentFiles,
pinned: pinnedFiles,
shortcuts,
transferRecent: transferRecentData,
} = await listFileManagerExportData(userId);
const alerts =
await createCurrentDismissedAlertRepository().listByUserId(userId);