mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: convert file manager into a plugin
This commit is contained in:
1 parent
b76da9ab52
commit
c85c1fdb86
259 files changed
+54155
-16791
No files matched your search
@@ -0,0 +1,143 @@
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import {
|
||||
promisifySftpMkdir,
|
||||
promisifySftpReaddir,
|
||||
promisifySftpStat,
|
||||
promisifySftpUnlink,
|
||||
promisifySftpRmdir,
|
||||
} from "./sftp-promisify.js";
|
||||
|
||||
const DEFAULT_DIR_MODE = 0o755;
|
||||
import type { SSHSession } from "./session.js";
|
||||
|
||||
/** What other plugins get from ctx.services.get("files.sftp", {userId}). */
|
||||
export interface FilesService {
|
||||
list: (
|
||||
hostId: number,
|
||||
path: string,
|
||||
) => Promise<
|
||||
Array<{ name: string; type: "file" | "directory" | "link"; size?: number }>
|
||||
>;
|
||||
read: (hostId: number, path: string) => Promise<string>;
|
||||
write: (hostId: number, path: string, content: string) => Promise<void>;
|
||||
stat: (
|
||||
hostId: number,
|
||||
path: string,
|
||||
) => Promise<{ size: number; isDirectory: boolean; mtime: number }>;
|
||||
delete: (hostId: number, path: string) => Promise<void>;
|
||||
mkdir: (hostId: number, path: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A short-lived SFTP connection for a service call, outside the browse
|
||||
* session map: a caller reaches this from any request the acting user makes,
|
||||
* not necessarily one that has an open file-manager tab.
|
||||
*/
|
||||
async function withServiceSftp<T>(
|
||||
ctx: PluginContext,
|
||||
hostId: number,
|
||||
fn: (sftp: import("ssh2").SFTPWrapper) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return ctx.ssh.withConnection<T, import("ssh2").Client>(
|
||||
hostId,
|
||||
{ pool: "file-manager-service", purpose: "file-manager" },
|
||||
(client) =>
|
||||
new Promise<T>((resolve, reject) => {
|
||||
client.sftp(async (err, sftp) => {
|
||||
if (err) return reject(err);
|
||||
try {
|
||||
resolve(await fn(sftp));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function createFilesService(
|
||||
ctx: PluginContext,
|
||||
deps: {
|
||||
sshSessions: Record<string, SSHSession>;
|
||||
verifySessionOwnership: (session: SSHSession, userId: string) => boolean;
|
||||
},
|
||||
): FilesService {
|
||||
void deps;
|
||||
return {
|
||||
async list(hostId, path) {
|
||||
return withServiceSftp(ctx, hostId, async (sftp) => {
|
||||
const entries = await promisifySftpReaddir(sftp, path);
|
||||
return entries
|
||||
.filter((e) => e.filename !== "." && e.filename !== "..")
|
||||
.map((e) => ({
|
||||
name: e.filename,
|
||||
type: e.attrs.isDirectory()
|
||||
? ("directory" as const)
|
||||
: e.attrs.isSymbolicLink()
|
||||
? ("link" as const)
|
||||
: ("file" as const),
|
||||
size: e.attrs.isDirectory() ? undefined : e.attrs.size,
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
async read(hostId, path) {
|
||||
return withServiceSftp(
|
||||
ctx,
|
||||
hostId,
|
||||
(sftp) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const stream = sftp.createReadStream(path);
|
||||
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
stream.on("end", () =>
|
||||
resolve(Buffer.concat(chunks).toString("utf8")),
|
||||
);
|
||||
stream.on("error", reject);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async write(hostId, path, content) {
|
||||
await withServiceSftp(
|
||||
ctx,
|
||||
hostId,
|
||||
(sftp) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const stream = sftp.createWriteStream(path, { flags: "w" });
|
||||
stream.on("close", () => resolve());
|
||||
stream.on("error", reject);
|
||||
stream.end(content, "utf8");
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async stat(hostId, path) {
|
||||
return withServiceSftp(ctx, hostId, async (sftp) => {
|
||||
const stats = await promisifySftpStat(sftp, path);
|
||||
return {
|
||||
size: stats.size,
|
||||
isDirectory: stats.isDirectory(),
|
||||
mtime: stats.mtime,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
async delete(hostId, path) {
|
||||
await withServiceSftp(ctx, hostId, async (sftp) => {
|
||||
const stats = await promisifySftpStat(sftp, path);
|
||||
if (stats.isDirectory()) {
|
||||
await promisifySftpRmdir(sftp, path);
|
||||
} else {
|
||||
await promisifySftpUnlink(sftp, path);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async mkdir(hostId, path) {
|
||||
await withServiceSftp(ctx, hostId, (sftp) =>
|
||||
promisifySftpMkdir(sftp, path, DEFAULT_DIR_MODE),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user