/// import { promises as fs } from "node:fs"; import * as path from "node:path"; import { createTRPCContext, procedure } from "@onlineworkspace/workspace-backend/src/systems/trpcRouter.ts"; import { initTRPC } from "@trpc/server"; import fastFolderSize from "fast-folder-size/sync.js"; import z from "zod"; const log = instance.log.createLogger("uk.ewsgit.files"); export const t = initTRPC.context>().create(); const router = t.router({ /* getFileGrid: procedure.input(z.object({ path: z.string(), sortBy: z.enum(["name"]) })).query(async (opt) => { const finalPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); if (!(await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalPath)).read) { // send a missing permissions notification return { type: "error" as const, message: "You cannot access this location!", icon: "folder_limited", }; } const THUMBNAIL_SIZE = 128; const output: { name: string; path: string; icon?: string; type: "directory" | "file" | "alias"; }[] = []; if (!fsExists(finalPath)) { return { type: "error" as const, message: "This directory does not exist!", icon: "folder_limited", }; } for (const item of await fs.readdir(finalPath)) { const itemPath = path.join(finalPath, item); const isDirectory = (await fs.lstat(itemPath)).isDirectory(); let icon: string; if (!isDirectory) { switch (instance.sys.filesystem.getFileType(itemPath)) { case "image": { icon = opt.ctx.instance.sys.configuration.backendUrl + (await instance.sys.image.serveImage(opt.ctx.userId, itemPath, { resize: { dimensions: (dimensions) => { let newWidth = THUMBNAIL_SIZE; let newHeight = THUMBNAIL_SIZE; if (dimensions.width > dimensions.height) { newWidth = THUMBNAIL_SIZE; newHeight = Math.round((dimensions.height / dimensions.width) * THUMBNAIL_SIZE); } if (dimensions.height > dimensions.width) { newHeight = THUMBNAIL_SIZE; newWidth = Math.round((dimensions.height / dimensions.width) * THUMBNAIL_SIZE); } return { width: newWidth, height: newHeight, }; }, }, })); } } } let itemName = item; const finalItemPath = path.relative(instance.sys.filesystem.FS_ROOT, itemPath); if (finalItemPath.split(itemName)[0] === "users/") { itemName += ` (${await (await instance.sys.users.getUserById(Number(itemName)))?.getUsername()})`; } output.push({ name: itemName, path: finalItemPath, // @ts-ignore icon: icon, type: isDirectory ? "directory" : "file", }); } if (output.length < 1) { return { type: "info" as const, message: "This directory is empty", icon: "folder", }; } return { type: "success" as const, items: output }; }), getFileList: procedure.input(z.object({ path: z.string(), sortBy: z.enum(["name"]) })).query(async (opt) => { const finalPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); if (!(await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalPath)).read) { // send a missing permissions notification return { type: "error" as const, message: "You cannot access this location!", icon: "folder_limited", }; } const THUMBNAIL_SIZE = 32; const output: { name: string; path: string; icon?: string; type: "directory" | "file" | "alias"; }[] = []; if (!fsExists(finalPath)) { return { type: "error" as const, message: "This directory does not exist!", icon: "folder_limited", }; } for (const item of await fs.readdir(finalPath)) { const itemPath = path.join(finalPath, item); const isDirectory = (await fs.lstat(itemPath)).isDirectory(); let icon: string; if (!isDirectory) { switch (instance.sys.filesystem.getFileType(itemPath)) { case "image": { icon = opt.ctx.instance.sys.configuration.backendUrl + (await instance.sys.image.serveImage(opt.ctx.userId, itemPath, { resize: { dimensions: (dimensions) => { let newWidth = THUMBNAIL_SIZE; let newHeight = THUMBNAIL_SIZE; if (dimensions.width > dimensions.height) { newWidth = THUMBNAIL_SIZE; newHeight = Math.round((dimensions.height / dimensions.width) * THUMBNAIL_SIZE); } if (dimensions.height > dimensions.width) { newHeight = THUMBNAIL_SIZE; newWidth = Math.round((dimensions.height / dimensions.width) * THUMBNAIL_SIZE); } return { width: newWidth, height: newHeight, }; }, }, })); } } } let itemName = item; const finalItemPath = path.relative(instance.sys.filesystem.FS_ROOT, itemPath); if (finalItemPath.split(itemName)[0] === "users/") { itemName += ` (${await (await instance.sys.users.getUserById(Number(itemName)))?.getUsername()})`; } output.push({ name: itemName, path: finalItemPath, // @ts-ignore icon: icon, type: isDirectory ? "directory" : "file", }); } if (output.length < 1) { return { type: "info" as const, message: "This directory is empty", icon: "folder", }; } return { type: "success" as const, items: output }; }), getRawFile: procedure.input(z.string()).query(async (opt) => { const finalPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input); if (!(await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalPath)).read) { return "You lack the required permissions to access this resource!"; } return opt.ctx.instance.sys.configuration.backendUrl + instance.sys.filesystem.serveFile(opt.ctx.userId, finalPath); }), batchMove: procedure.input(z.object({ path: z.string(), newPath: z.string() }).array()).mutation(async (opt) => { for (const item of opt.input) { if (item.path === item.newPath) continue; // TODO: send a failure notification if (path.join(item.newPath, "..") === "users") continue; if (path.join(item.path, "..") === "users") continue; const finalPath = path.join(instance.sys.filesystem.FS_ROOT, item.path); const finalNewPath = path.join(instance.sys.filesystem.FS_ROOT, item.newPath); try { if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalPath)).read) { if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalNewPath)).write) { await fs.rename(finalPath, finalNewPath); } else { // send a missing permissions notification return false; } } else { // send a missing permissions notification return false; } } catch (err) { log.error(err); } } }), batchCopy: procedure.input(z.object({ path: z.string(), newPath: z.string() }).array()).mutation(async (opt) => { for (const item of opt.input) { if (item.path === item.newPath) continue; // TODO: send a failure notification if (path.join(item.newPath, "..") === "users") continue; if (path.join(item.path, "..") === "users") continue; const finalPath = path.join(instance.sys.filesystem.FS_ROOT, item.path); const finalNewPath = path.join(instance.sys.filesystem.FS_ROOT, item.newPath); try { if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalPath)).read) { if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, finalNewPath)).write) { await fs.cp(finalPath, finalNewPath, { recursive: true }); } else { // send a missing permissions notification return false; } } else { // send a missing permissions notification return false; } } catch (err) { log.error(err); } } return true; }), batchDelete: procedure.input(z.string().array()).mutation(async (opt) => { for (const item of opt.input) { const itemPath = path.join(instance.sys.filesystem.FS_ROOT, item); try { if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, itemPath)).write) { await fs.rm(itemPath, { recursive: true }); } else { // send a missing permissions notification return false; } } catch (err) { log.error(err); } } return true; }), createFile: procedure.input(z.object({ filePath: z.string() })).mutation(async (opt) => { const filePath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.filePath); if (fsExists(filePath)) return false; if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, filePath)).write) { await fs.writeFile(filePath, ""); } else { // send a missing permissions notification return false; } return true; }), createDirectory: procedure.input(z.object({ directoryPath: z.string() })).mutation(async (opt) => { const filePath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.directoryPath); if (fsExists(filePath)) return false; if ((await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, filePath)).write) { await fs.mkdir(filePath, { recursive: true }); } else { // send a missing permissions notification return false; } return true; }), getHome: procedure.query(async (opt) => { return `/users/${opt.ctx.userId}/fs`; }), getPlaces: procedure.query(async (opt) => { return [ { icon: "house", path: `/users/${opt.ctx.userId}/fs`, name: "Home", }, { icon: "data_usage", path: `/`, name: "Root", }, ]; }), uploadFile: procedure.input(octetInputParser).mutation(async (opt) => { const uuid = randomUUIDv7(); const uploadFilePath = path.join((await opt.ctx.user()).getPath(), `system/temp/${uuid}`); log.info(`Uploading file ${uuid}`); const fileResponse = new Response(opt.input); await fs.writeFile(uploadFilePath, await fileResponse.bytes()); return { id: uuid }; }), setUploadMetadata: procedure.input(z.object({ id: z.string(), path: z.string(), lastModified: z.number() })).mutation(async (opt) => { const uploadFilePath = path.join((await opt.ctx.user()).getPath(), `system/temp/${opt.input.id}`); const actualFilePath = path.join(instance.sys.filesystem.FS_ROOT, `${opt.input.path}`); const actualFilePathParentDir = path.join(actualFilePath, ".."); if (!(await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, actualFilePath)).write) { return false; } if (!fsExists(actualFilePathParentDir)) { await fs.mkdir(actualFilePathParentDir, { recursive: true }); } log.info(`Applying metadata to file (${opt.input.id}) '${actualFilePath}'`); await fs.rename(uploadFilePath, actualFilePath); await fs.utimes(actualFilePath, 0, opt.input.lastModified); return true; }), */ userPreferences: { get: procedure.query(async (opt) => { return { showWelcome: false, homePath: `remote:/users/${opt.ctx.userId}/fs`, pinnedDirectories: [`remote:/users/${opt.ctx.userId}/fs/Photos`, `remote:/users/${opt.ctx.userId}/fs/Documents`], }; }), }, readDirectory: procedure .input(z.object({ path: z.string() })) .output( z .object({ items: z.undefined(), status: z.enum(["missing_permission", "invalid_path"]) }) .or(z.object({ items: z.string().array(), status: z.literal("ok") })), ) .query(async (opt) => { const resolvedPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); log.info(`Read Directory requested by user ${opt.ctx.userId} ${resolvedPath}`); const userPermissions = await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, resolvedPath); if (!userPermissions.read) { return { status: "missing_permission" as const }; } try { const directoryContents = await fs.readdir(resolvedPath, { withFileTypes: true }); directoryContents.sort((a, b) => { if (a.isDirectory() && !b.isDirectory()) return -1; if (!a.isDirectory() && b.isDirectory()) return 1; return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); }); return { items: directoryContents.map((ent) => ent.name), status: "ok" }; } catch (_) { return { status: "invalid_path" as const }; } }), view: { getEntry: procedure .input(z.object({ path: z.string(), thumbnailSize: z.number().optional() })) .output( z .object({ status: z.literal("ok"), data: z.object({ path: z.string(), type: z.enum(["file", "directory", "link"]), thumbnail: z.string().optional(), shared: z.boolean().optional(), size: z.number().optional(), hidden: z.boolean().optional(), createdAt: z.number().optional(), modifiedAt: z.number().optional(), }), }) .or(z.object({ status: z.enum(["missing_permission", "invalid_path"]) })), ) .query(async (opt) => { const resolvedPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); log.info(`View entry requested by user ${opt.ctx.userId} ${resolvedPath}`); const userPermissions = await instance.sys.filesystem.getUserPermissions(opt.ctx.userId, resolvedPath); if (!userPermissions.read) { return { status: "missing_permission" as const, }; } try { const itemStats = await fs.lstat(resolvedPath); return { status: "ok" as const, data: { path: opt.input.path, type: itemStats.isSymbolicLink() ? "link" : itemStats.isDirectory() ? "directory" : itemStats.isFile() ? "file" : "file", // TODO: implement sharing first shared: false, size: itemStats.size, thumbnail: opt.input.thumbnailSize !== undefined && itemStats.isFile() ? instance.sys.filesystem.getFileType(opt.input.path) === "image" ? await instance.sys.image.serveImage(opt.ctx.userId, resolvedPath, { resize: { fit: "cover", position: "centre", dimensions: { width: opt.input.thumbnailSize, height: Math.floor((opt.input.thumbnailSize / 16) * 9) }, changeFormatTo: "webp", }, }) : undefined : undefined, hidden: path.basename(opt.input.path).startsWith("."), createdAt: itemStats.ctimeMs, modifiedAt: itemStats.atimeMs, }, }; } catch (_) { return { status: "invalid_path" as const, }; } }), getGalleryItem: procedure.input(z.object({ path: z.string().or(z.undefined()), height: z.number() })).query(async (opt) => { let dimensions: { width: number; height: number } = { width: 0, height: 0 }; if (opt.input.path === undefined) { return { image: "/assets/generic_background.svg", dimensions }; } const resolvedPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); return { image: instance.sys.filesystem.getFileType(opt.input.path) === "image" ? await instance.sys.image.serveImage(opt.ctx.userId, resolvedPath, { resize: { fit: "cover", position: "centre", dimensions: ({ width: fileWidth, height: fileHeight }) => { dimensions = { width: fileWidth, height: fileHeight }; return { width: Math.floor((fileWidth / fileHeight) * opt.input.height), height: opt.input.height }; }, changeFormatTo: "webp", }, }) : undefined, dimensions, }; }), }, quota: procedure.output(z.object({ currentUsage: z.number(), maximum: z.number() })).query(async (opt) => { const quotaMax = (await (await opt.ctx.user()).getQuota()) || 8589934592; const resp = { maximum: Number(quotaMax), currentUsage: fastFolderSize((await opt.ctx.user()).getPath()) || -1, }; return resp; }), previewDialog: { get: procedure.input(z.object({ path: z.string() })).query(async (opt) => { const resolvedPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); const pathStat = await fs.lstat(resolvedPath); const isDirectory = pathStat.isDirectory(); const fileType = !isDirectory ? instance.sys.filesystem.getFileType(resolvedPath) : "directory"; let imageDimensions: { width: number; height: number } = { width: 0, height: 0 }; return { status: "ok" as const, data: { assets: fileType === "image" ? { original: await instance.sys.image.serveImage(opt.ctx.userId, resolvedPath), small: await instance.sys.image.serveImage(opt.ctx.userId, resolvedPath, { resize: { dimensions: (original) => { imageDimensions = original; return { width: 1024, height: Math.floor((original.height / original.width) * 1024) }; }, }, }), } : undefined, metadata: { size: pathStat.size, type: pathStat.isSymbolicLink() ? "link" : pathStat.isDirectory() ? "directory" : pathStat.isFile() ? "file" : "file", itemCount: isDirectory ? (await fs.readdir(resolvedPath)).length : undefined, pixelate: imageDimensions.width !== 0 && imageDimensions.height !== 0 && imageDimensions.width < 640 && imageDimensions.height < 640, }, }, }; }), }, create: { directory: procedure.input(z.object({ path: z.string() })).mutation(async (opt) => { const resolvedPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); try { await fs.mkdir(resolvedPath, { recursive: true }); return { status: "ok" as const, }; } catch (err) { console.log("dir creation error", err); return { status: "already_exists" as const, }; } }), file: procedure.input(z.object({ path: z.string(), template: z.string().or(z.undefined()) })).mutation(async (opt) => { const resolvedPath = path.join(instance.sys.filesystem.FS_ROOT, opt.input.path); const FILE_TEMPLATE_PATH = path.join(instance.sys.filesystem.SYSTEM_PATH, "fs_template_files"); let templateContents = ""; if (opt.input.template !== undefined) { const templateFilePath = path.join(FILE_TEMPLATE_PATH, opt.input.template); if (await fs.exists(templateFilePath)) { templateContents = (await fs.readFile(templateFilePath)).toString(); } } try { if (await fs.exists(resolvedPath)) { return { status: "already_exists" as const }; } await fs.writeFile(resolvedPath, templateContents); return { status: "ok" as const, }; } catch (err) { console.log("file creation error", err); return { status: "already_exists" as const, }; } }), }, }); export type TRPCRouter = typeof router; instance.sys.tRPC.routers.push({ basePath: "/api/app/uk.ewsgit.files", router: router, createContext: createTRPCContext(instance), });