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

+2 -14
View File
@@ -14,7 +14,6 @@ import { CommandHistoryRepository } from "./command-history-repository.js";
import { CredentialRepository } from "./credential-repository.js";
import { DashboardServiceLinkRepository } from "./dashboard-service-link-repository.js";
import { DismissedAlertRepository } from "./dismissed-alert-repository.js";
import { FileManagerBookmarkRepository } from "./file-manager-bookmark-repository.js";
import { HomepageItemRepository } from "./homepage-item-repository.js";
import { HomepageLayoutRepository } from "./homepage-layout-repository.js";
import { HostFolderRepository } from "./host-folder-repository.js";
@@ -57,7 +56,6 @@ import { SsoProviderRepository } from "./sso-provider-repository.js";
import { TermixIdentityCaRepository } from "./termix-identity-ca-repository.js";
import { TermixIdentityRepository } from "./termix-identity-repository.js";
import { TmuxSessionTagRepository } from "./tmux-session-tag-repository.js";
import { TransferRecentRepository } from "./transfer-recent-repository.js";
import { TrustedDeviceRepository } from "./trusted-device-repository.js";
import { UserDataExportRepository } from "./user-data-export-repository.js";
import { UserPreferenceRepository } from "./user-preference-repository.js";
@@ -229,12 +227,7 @@ export function createCurrentDismissedAlertRepository(): DismissedAlertRepositor
);
}
export function createCurrentFileManagerBookmarkRepository(): FileManagerBookmarkRepository {
return new FileManagerBookmarkRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("file_manager_bookmarks_repository_write"),
);
}
// File manager recent/pinned/shortcuts moved to the file-manager plugin.
export function createCurrentHomepageItemRepository(): HomepageItemRepository {
return new HomepageItemRepository(
@@ -512,12 +505,7 @@ export function createCurrentTmuxSessionTagRepository(): TmuxSessionTagRepositor
);
}
export function createCurrentTransferRecentRepository(): TransferRecentRepository {
return new TransferRecentRepository(
createCurrentRepositoryContext(),
createCurrentRepositoryWriteHook("transfer_recent_repository_write"),
);
}
// transfer_recent moved to the file-manager plugin.
export function createCurrentTrustedDeviceRepository(): TrustedDeviceRepository {
return new TrustedDeviceRepository(
@@ -1,522 +0,0 @@
import { and, desc, eq, inArray } from "drizzle-orm";
import {
fileManagerPinned,
fileManagerRecent,
fileManagerShortcuts,
} from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
export type FileManagerRecentRecord = typeof fileManagerRecent.$inferSelect;
export type FileManagerPinnedRecord = typeof fileManagerPinned.$inferSelect;
export type FileManagerShortcutRecord =
typeof fileManagerShortcuts.$inferSelect;
export interface FileManagerBookmarkInput {
hostId: number;
path: string;
name?: string | null;
}
function resolveBookmarkName(path: string, name?: string | null): string {
return name || path.split("/").pop() || "Unknown";
}
export class FileManagerBookmarkRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async listRecentForHost(
userId: string,
hostId: number,
limit = 20,
): Promise<FileManagerRecentRecord[]> {
return this.context.drizzle
.select()
.from(fileManagerRecent)
.where(
and(
eq(fileManagerRecent.userId, userId),
eq(fileManagerRecent.hostId, hostId),
),
)
.orderBy(desc(fileManagerRecent.lastOpened))
.limit(limit);
}
async listRecentByUserId(userId: string): Promise<FileManagerRecentRecord[]> {
return this.context.drizzle
.select()
.from(fileManagerRecent)
.where(eq(fileManagerRecent.userId, userId));
}
async upsertRecent(
userId: string,
input: FileManagerBookmarkInput,
lastOpened = new Date().toISOString(),
): Promise<void> {
const [existing] = await this.context.drizzle
.select({ id: fileManagerRecent.id })
.from(fileManagerRecent)
.where(
and(
eq(fileManagerRecent.userId, userId),
eq(fileManagerRecent.hostId, input.hostId),
eq(fileManagerRecent.path, input.path),
),
)
.limit(1);
if (existing) {
await this.context.drizzle
.update(fileManagerRecent)
.set({ lastOpened })
.where(eq(fileManagerRecent.id, existing.id));
} else {
await this.context.drizzle.insert(fileManagerRecent).values({
userId,
hostId: input.hostId,
path: input.path,
name: resolveBookmarkName(input.path, input.name),
lastOpened,
});
}
await this.afterWrite();
}
async createRecentForImport(
userId: string,
input: FileManagerBookmarkInput,
lastOpened = new Date().toISOString(),
): Promise<boolean> {
const exists = await this.existsRecentImportItem(userId, input);
if (exists) {
return false;
}
await this.context.drizzle.insert(fileManagerRecent).values({
userId,
hostId: input.hostId,
path: input.path,
name: resolveBookmarkName(input.path, input.name),
lastOpened,
});
await this.afterWrite();
return true;
}
async deleteRecentForHostPath(
userId: string,
input: Pick<FileManagerBookmarkInput, "hostId" | "path">,
): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerRecent)
.where(
and(
eq(fileManagerRecent.userId, userId),
eq(fileManagerRecent.hostId, input.hostId),
eq(fileManagerRecent.path, input.path),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async listPinnedForHost(
userId: string,
hostId: number,
): Promise<FileManagerPinnedRecord[]> {
return this.context.drizzle
.select()
.from(fileManagerPinned)
.where(
and(
eq(fileManagerPinned.userId, userId),
eq(fileManagerPinned.hostId, hostId),
),
)
.orderBy(desc(fileManagerPinned.pinnedAt));
}
async listPinnedByUserId(userId: string): Promise<FileManagerPinnedRecord[]> {
return this.context.drizzle
.select()
.from(fileManagerPinned)
.where(eq(fileManagerPinned.userId, userId));
}
async createPinned(
userId: string,
input: FileManagerBookmarkInput,
pinnedAt = new Date().toISOString(),
): Promise<boolean> {
const exists = await this.existsPinned(userId, input.hostId, input.path);
if (exists) {
return false;
}
await this.context.drizzle.insert(fileManagerPinned).values({
userId,
hostId: input.hostId,
path: input.path,
name: resolveBookmarkName(input.path, input.name),
pinnedAt,
});
await this.afterWrite();
return true;
}
async createPinnedForImport(
userId: string,
input: FileManagerBookmarkInput,
pinnedAt = new Date().toISOString(),
): Promise<boolean> {
const exists = await this.existsPinnedImportItem(userId, input);
if (exists) {
return false;
}
await this.context.drizzle.insert(fileManagerPinned).values({
userId,
hostId: input.hostId,
path: input.path,
name: resolveBookmarkName(input.path, input.name),
pinnedAt,
});
await this.afterWrite();
return true;
}
async deletePinnedForHostPath(
userId: string,
input: Pick<FileManagerBookmarkInput, "hostId" | "path">,
): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerPinned)
.where(
and(
eq(fileManagerPinned.userId, userId),
eq(fileManagerPinned.hostId, input.hostId),
eq(fileManagerPinned.path, input.path),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async listShortcutsForHost(
userId: string,
hostId: number,
): Promise<FileManagerShortcutRecord[]> {
return this.context.drizzle
.select()
.from(fileManagerShortcuts)
.where(
and(
eq(fileManagerShortcuts.userId, userId),
eq(fileManagerShortcuts.hostId, hostId),
),
)
.orderBy(desc(fileManagerShortcuts.createdAt));
}
async listShortcutsByUserId(
userId: string,
): Promise<FileManagerShortcutRecord[]> {
return this.context.drizzle
.select()
.from(fileManagerShortcuts)
.where(eq(fileManagerShortcuts.userId, userId));
}
async createShortcut(
userId: string,
input: FileManagerBookmarkInput,
createdAt = new Date().toISOString(),
): Promise<boolean> {
const exists = await this.existsShortcut(userId, input.hostId, input.path);
if (exists) {
return false;
}
await this.context.drizzle.insert(fileManagerShortcuts).values({
userId,
hostId: input.hostId,
path: input.path,
name: resolveBookmarkName(input.path, input.name),
createdAt,
});
await this.afterWrite();
return true;
}
async createShortcutForImport(
userId: string,
input: FileManagerBookmarkInput,
createdAt = new Date().toISOString(),
): Promise<boolean> {
const exists = await this.existsShortcutImportItem(userId, input);
if (exists) {
return false;
}
await this.context.drizzle.insert(fileManagerShortcuts).values({
userId,
hostId: input.hostId,
path: input.path,
name: resolveBookmarkName(input.path, input.name),
createdAt,
});
await this.afterWrite();
return true;
}
async deleteShortcutForHostPath(
userId: string,
input: Pick<FileManagerBookmarkInput, "hostId" | "path">,
): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerShortcuts)
.where(
and(
eq(fileManagerShortcuts.userId, userId),
eq(fileManagerShortcuts.hostId, input.hostId),
eq(fileManagerShortcuts.path, input.path),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async deleteByUserId(userId: string): Promise<number> {
const total =
(await this.deleteRecentByUserId(userId)) +
(await this.deletePinnedByUserId(userId)) +
(await this.deleteShortcutsByUserId(userId));
if (total > 0) {
await this.afterWrite();
}
return total;
}
async deleteByHostId(hostId: number): Promise<number> {
const total =
(await this.deleteRecentByHostId(hostId)) +
(await this.deletePinnedByHostId(hostId)) +
(await this.deleteShortcutsByHostId(hostId));
if (total > 0) {
await this.afterWrite();
}
return total;
}
async deleteByHostIds(hostIds: number[]): Promise<number> {
if (hostIds.length === 0) {
return 0;
}
const total =
(await this.deleteRecentByHostIds(hostIds)) +
(await this.deletePinnedByHostIds(hostIds)) +
(await this.deleteShortcutsByHostIds(hostIds));
if (total > 0) {
await this.afterWrite();
}
return total;
}
private async existsPinned(
userId: string,
hostId: number,
path: string,
): Promise<boolean> {
const rows = await this.context.drizzle
.select({ id: fileManagerPinned.id })
.from(fileManagerPinned)
.where(
and(
eq(fileManagerPinned.userId, userId),
eq(fileManagerPinned.hostId, hostId),
eq(fileManagerPinned.path, path),
),
)
.limit(1);
return rows.length > 0;
}
private async existsRecentImportItem(
userId: string,
input: FileManagerBookmarkInput,
): Promise<boolean> {
const rows = await this.context.drizzle
.select({ id: fileManagerRecent.id })
.from(fileManagerRecent)
.where(
and(
eq(fileManagerRecent.userId, userId),
eq(fileManagerRecent.path, input.path),
eq(
fileManagerRecent.name,
resolveBookmarkName(input.path, input.name),
),
),
)
.limit(1);
return rows.length > 0;
}
private async existsPinnedImportItem(
userId: string,
input: FileManagerBookmarkInput,
): Promise<boolean> {
const rows = await this.context.drizzle
.select({ id: fileManagerPinned.id })
.from(fileManagerPinned)
.where(
and(
eq(fileManagerPinned.userId, userId),
eq(fileManagerPinned.path, input.path),
eq(
fileManagerPinned.name,
resolveBookmarkName(input.path, input.name),
),
),
)
.limit(1);
return rows.length > 0;
}
private async existsShortcutImportItem(
userId: string,
input: FileManagerBookmarkInput,
): Promise<boolean> {
const rows = await this.context.drizzle
.select({ id: fileManagerShortcuts.id })
.from(fileManagerShortcuts)
.where(
and(
eq(fileManagerShortcuts.userId, userId),
eq(fileManagerShortcuts.path, input.path),
eq(
fileManagerShortcuts.name,
resolveBookmarkName(input.path, input.name),
),
),
)
.limit(1);
return rows.length > 0;
}
private async existsShortcut(
userId: string,
hostId: number,
path: string,
): Promise<boolean> {
const rows = await this.context.drizzle
.select({ id: fileManagerShortcuts.id })
.from(fileManagerShortcuts)
.where(
and(
eq(fileManagerShortcuts.userId, userId),
eq(fileManagerShortcuts.hostId, hostId),
eq(fileManagerShortcuts.path, path),
),
)
.limit(1);
return rows.length > 0;
}
private async deleteRecentByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerRecent)
.where(eq(fileManagerRecent.userId, userId));
return rowsAffected(result);
}
private async deletePinnedByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerPinned)
.where(eq(fileManagerPinned.userId, userId));
return rowsAffected(result);
}
private async deleteShortcutsByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerShortcuts)
.where(eq(fileManagerShortcuts.userId, userId));
return rowsAffected(result);
}
private async deleteRecentByHostId(hostId: number): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerRecent)
.where(eq(fileManagerRecent.hostId, hostId));
return rowsAffected(result);
}
private async deletePinnedByHostId(hostId: number): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerPinned)
.where(eq(fileManagerPinned.hostId, hostId));
return rowsAffected(result);
}
private async deleteShortcutsByHostId(hostId: number): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerShortcuts)
.where(eq(fileManagerShortcuts.hostId, hostId));
return rowsAffected(result);
}
private async deleteRecentByHostIds(hostIds: number[]): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerRecent)
.where(inArray(fileManagerRecent.hostId, hostIds));
return rowsAffected(result);
}
private async deletePinnedByHostIds(hostIds: number[]): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerPinned)
.where(inArray(fileManagerPinned.hostId, hostIds));
return rowsAffected(result);
}
private async deleteShortcutsByHostIds(hostIds: number[]): Promise<number> {
const result = await this.context.drizzle
.delete(fileManagerShortcuts)
.where(inArray(fileManagerShortcuts.hostId, hostIds));
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}
@@ -1,168 +0,0 @@
import { and, desc, eq, inArray, or } from "drizzle-orm";
import { transferRecent } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
export type TransferRecentRecord = typeof transferRecent.$inferSelect;
export interface TransferRecentDestinationInput {
sourceHostId: number;
destHostId: number;
destPath: string;
destPathLabel?: string | null;
}
export class TransferRecentRepository {
constructor(
private readonly context: DatabaseContext,
private readonly onWrite?: () => void | Promise<void>,
) {}
async listByUserId(userId: string): Promise<TransferRecentRecord[]> {
return this.context.drizzle
.select()
.from(transferRecent)
.where(eq(transferRecent.userId, userId));
}
async listBySourceHost(
userId: string,
sourceHostId: number,
limit = 10,
): Promise<TransferRecentRecord[]> {
return this.context.drizzle
.select()
.from(transferRecent)
.where(
and(
eq(transferRecent.userId, userId),
eq(transferRecent.sourceHostId, sourceHostId),
),
)
.orderBy(desc(transferRecent.lastUsed))
.limit(limit);
}
async upsertForDestination(
userId: string,
input: TransferRecentDestinationInput,
lastUsed = new Date().toISOString(),
): Promise<void> {
const [existing] = await this.context.drizzle
.select({ id: transferRecent.id })
.from(transferRecent)
.where(
and(
eq(transferRecent.userId, userId),
eq(transferRecent.sourceHostId, input.sourceHostId),
eq(transferRecent.destHostId, input.destHostId),
eq(transferRecent.destPath, input.destPath),
),
)
.limit(1);
if (existing) {
await this.context.drizzle
.update(transferRecent)
.set({ lastUsed })
.where(eq(transferRecent.id, existing.id));
} else {
await this.context.drizzle.insert(transferRecent).values({
userId,
sourceHostId: input.sourceHostId,
destHostId: input.destHostId,
destPath: input.destPath,
destPathLabel: input.destPathLabel || input.destPath,
lastUsed,
});
}
await this.afterWrite();
}
async pruneSourceHost(
userId: string,
sourceHostId: number,
keep = 10,
): Promise<number> {
const rows = await this.context.drizzle
.select({ id: transferRecent.id })
.from(transferRecent)
.where(
and(
eq(transferRecent.userId, userId),
eq(transferRecent.sourceHostId, sourceHostId),
),
)
.orderBy(desc(transferRecent.lastUsed));
const idsToDelete = rows.slice(keep).map((row) => row.id);
if (idsToDelete.length === 0) {
return 0;
}
const result = await this.context.drizzle
.delete(transferRecent)
.where(inArray(transferRecent.id, idsToDelete));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async deleteByUserId(userId: string): Promise<number> {
const result = await this.context.drizzle
.delete(transferRecent)
.where(eq(transferRecent.userId, userId));
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async deleteByHostId(hostId: number): Promise<number> {
const result = await this.context.drizzle
.delete(transferRecent)
.where(
or(
eq(transferRecent.sourceHostId, hostId),
eq(transferRecent.destHostId, hostId),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
async deleteByHostIds(hostIds: number[]): Promise<number> {
if (hostIds.length === 0) {
return 0;
}
const result = await this.context.drizzle
.delete(transferRecent)
.where(
or(
inArray(transferRecent.sourceHostId, hostIds),
inArray(transferRecent.destHostId, hostIds),
),
);
if (rowsAffected(result) > 0) {
await this.afterWrite();
}
return rowsAffected(result);
}
private async afterWrite(): Promise<void> {
await this.onWrite?.();
}
}