dsh-plugin-workbench 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +17 -7
- package/lib/client.js +641 -160
- package/lib/client.js.map +1 -1
- package/lib/index.js +169 -6
- package/package.json +1 -1
- package/src/client/FileExplorer.tsx +357 -64
- package/src/client/FilePreview.tsx +171 -34
- package/src/client/files.module.css +144 -3
- package/src/client/index.ts +5 -1
- package/src/client/locales.ts +30 -0
- package/src/client/store.ts +25 -0
- package/src/dsh.d.ts +9 -0
- package/src/index.ts +204 -6
package/lib/index.js
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
1
2
|
//#region src/index.ts
|
|
2
3
|
const name = "dsh-plugin-workbench";
|
|
3
|
-
const inject = [
|
|
4
|
+
const inject = [
|
|
5
|
+
"fs",
|
|
6
|
+
"connection",
|
|
7
|
+
"webServer"
|
|
8
|
+
];
|
|
4
9
|
/** Loopback-only logical RPC channel. */
|
|
5
10
|
const CHANNEL = "/dsh-plugin-files";
|
|
6
11
|
/** Files larger than this are never read for preview (client shows size + hint). */
|
|
7
12
|
const MAX_PREVIEW_BYTES = 524288;
|
|
13
|
+
/** Same-origin route serving raw bytes for image files (see module doc). */
|
|
14
|
+
const RAW_PREFIX = "/dsh-plugin-files/raw";
|
|
15
|
+
/** Images larger than this are never served to the preview (browser shows a hint). */
|
|
16
|
+
const MAX_IMAGE_BYTES = 20971520;
|
|
17
|
+
const IMAGE_MIME = {
|
|
18
|
+
png: "image/png",
|
|
19
|
+
jpg: "image/jpeg",
|
|
20
|
+
jpeg: "image/jpeg",
|
|
21
|
+
gif: "image/gif",
|
|
22
|
+
webp: "image/webp",
|
|
23
|
+
avif: "image/avif",
|
|
24
|
+
bmp: "image/bmp",
|
|
25
|
+
ico: "image/x-icon",
|
|
26
|
+
svg: "image/svg+xml"
|
|
27
|
+
};
|
|
28
|
+
/** MIME type for a path's extension when it names a previewable image; undefined otherwise. */
|
|
29
|
+
function imageMimeOf(path) {
|
|
30
|
+
const idx = path.lastIndexOf(".");
|
|
31
|
+
if (idx < 0 || idx === path.length - 1) return void 0;
|
|
32
|
+
return IMAGE_MIME[path.slice(idx + 1).toLowerCase()];
|
|
33
|
+
}
|
|
34
|
+
/** True when the path names a previewable image file. */
|
|
35
|
+
function isImagePath(path) {
|
|
36
|
+
return imageMimeOf(path) !== void 0;
|
|
37
|
+
}
|
|
8
38
|
/** Map an fs entry type onto the wire `kind` union. */
|
|
9
39
|
function kindOf(type) {
|
|
10
40
|
if (type === "directory") return "dir";
|
|
@@ -42,7 +72,15 @@ const FS_ERROR_MESSAGES = {
|
|
|
42
72
|
FS_PERMISSION_DENIED: "permission denied",
|
|
43
73
|
FS_SANDBOX_DENIED: "sandbox denied",
|
|
44
74
|
FS_ABORTED: "aborted",
|
|
45
|
-
FS_IO_ERROR: "io error"
|
|
75
|
+
FS_IO_ERROR: "io error",
|
|
76
|
+
EEXIST: "file or folder already exists",
|
|
77
|
+
ENOENT: "path does not exist",
|
|
78
|
+
ENOTEMPTY: "folder is not empty",
|
|
79
|
+
EPERM: "permission denied",
|
|
80
|
+
EACCES: "permission denied",
|
|
81
|
+
ENOTDIR: "not a directory",
|
|
82
|
+
EISDIR: "is a directory",
|
|
83
|
+
EBUSY: "file is in use"
|
|
46
84
|
};
|
|
47
85
|
/** Human-readable message for a thrown value, honoring the fs error code taxonomy. */
|
|
48
86
|
function mapError(error) {
|
|
@@ -80,10 +118,65 @@ function apply(ctx) {
|
|
|
80
118
|
const handler = async (endpoint, payload, signal) => {
|
|
81
119
|
if (endpoint === "list") return listDir(ctx, payload, signal);
|
|
82
120
|
if (endpoint === "read") return readFile(ctx, payload, signal);
|
|
83
|
-
if (endpoint === "write") return writeFile(ctx, payload, signal);
|
|
121
|
+
if (endpoint === "write") return writeFile$1(ctx, payload, signal);
|
|
122
|
+
if (endpoint === "createFile") return createFile(ctx, payload, signal);
|
|
123
|
+
if (endpoint === "createDir") return createDir(ctx, payload, signal);
|
|
124
|
+
if (endpoint === "rename") return renameEntry(ctx, payload, signal);
|
|
125
|
+
if (endpoint === "delete") return deleteEntry(ctx, payload, signal);
|
|
84
126
|
return fail(`unknown endpoint: ${endpoint}`);
|
|
85
127
|
};
|
|
86
|
-
ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" });
|
|
128
|
+
ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "dsh-plugin-workbench: files rpc channel");
|
|
129
|
+
ctx.effect(() => ctx.webServer.register({
|
|
130
|
+
kind: "prefix",
|
|
131
|
+
path: RAW_PREFIX,
|
|
132
|
+
handler: (req, res) => {
|
|
133
|
+
serveRaw(ctx, req, res);
|
|
134
|
+
}
|
|
135
|
+
}), "dsh-plugin-workbench: raw image route");
|
|
136
|
+
}
|
|
137
|
+
async function serveRaw(ctx, req, res) {
|
|
138
|
+
const text = (code, body) => {
|
|
139
|
+
res.writeHead(code, { "content-type": "text/plain; charset=utf-8" });
|
|
140
|
+
res.end(body);
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
if ((req.method ?? "GET").toUpperCase() !== "GET") {
|
|
144
|
+
text(405, "method not allowed");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const rest = new URL(req.url ?? "/", "http://dsh.internal").pathname.slice(21).replace(/^\/+/, "");
|
|
148
|
+
if (rest.length === 0) {
|
|
149
|
+
text(404, "not found");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const path = decodeURIComponent(rest);
|
|
153
|
+
const mime = imageMimeOf(path);
|
|
154
|
+
if (mime === void 0) {
|
|
155
|
+
text(404, "not an image");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const target = await ctx.fs.resolve(path);
|
|
159
|
+
const info = await ctx.fs.stat(target);
|
|
160
|
+
if (info === void 0 || info.type !== "file") {
|
|
161
|
+
text(404, "not found");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if ((info.size ?? 0) > 20971520) {
|
|
165
|
+
text(413, "image too large");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const bytes = await ctx.fs.readBytes(target, void 0, MAX_IMAGE_BYTES);
|
|
169
|
+
res.writeHead(200, {
|
|
170
|
+
"content-type": mime,
|
|
171
|
+
"content-length": bytes.byteLength,
|
|
172
|
+
"cache-control": "private, max-age=300",
|
|
173
|
+
"x-content-type-options": "nosniff"
|
|
174
|
+
});
|
|
175
|
+
res.end(Buffer.from(bytes));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (!res.headersSent) text(500, "internal error");
|
|
178
|
+
else res.destroy();
|
|
179
|
+
}
|
|
87
180
|
}
|
|
88
181
|
async function listDir(ctx, payload, signal) {
|
|
89
182
|
const path = pathOf(payload);
|
|
@@ -153,7 +246,7 @@ async function readFile(ctx, payload, signal) {
|
|
|
153
246
|
return fail(mapError(error));
|
|
154
247
|
}
|
|
155
248
|
}
|
|
156
|
-
async function writeFile(ctx, payload, signal) {
|
|
249
|
+
async function writeFile$1(ctx, payload, signal) {
|
|
157
250
|
const path = pathOf(payload);
|
|
158
251
|
const content = typeof payload === "object" && payload !== null ? payload.content : void 0;
|
|
159
252
|
if (path === void 0) return fail("write: payload.path must be a non-empty string");
|
|
@@ -175,5 +268,75 @@ async function writeFile(ctx, payload, signal) {
|
|
|
175
268
|
return fail(mapError(error));
|
|
176
269
|
}
|
|
177
270
|
}
|
|
271
|
+
async function createFile(ctx, payload, signal) {
|
|
272
|
+
const path = pathOf(payload);
|
|
273
|
+
if (path === void 0) return fail("createFile: payload.path must be a non-empty string");
|
|
274
|
+
try {
|
|
275
|
+
const target = await ctx.fs.resolve(path, { signal });
|
|
276
|
+
const osPath = ctx.fs.processPath(target);
|
|
277
|
+
await writeFile(osPath, "", { flag: "wx" });
|
|
278
|
+
return {
|
|
279
|
+
ok: true,
|
|
280
|
+
value: { path: osPath }
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return fail(mapError(error));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function createDir(ctx, payload, signal) {
|
|
287
|
+
const path = pathOf(payload);
|
|
288
|
+
if (path === void 0) return fail("createDir: payload.path must be a non-empty string");
|
|
289
|
+
try {
|
|
290
|
+
const target = await ctx.fs.resolve(path, { signal });
|
|
291
|
+
const osPath = ctx.fs.processPath(target);
|
|
292
|
+
await mkdir(osPath);
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
value: { path: osPath }
|
|
296
|
+
};
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return fail(mapError(error));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function renameEntry(ctx, payload, signal) {
|
|
302
|
+
const path = pathOf(payload);
|
|
303
|
+
const to = typeof payload === "object" && payload !== null ? payload.to : void 0;
|
|
304
|
+
if (path === void 0) return fail("rename: payload.path must be a non-empty string");
|
|
305
|
+
if (typeof to !== "string" || to.trim().length === 0) return fail("rename: payload.to must be a non-empty string");
|
|
306
|
+
try {
|
|
307
|
+
const target = await ctx.fs.resolve(path, { signal });
|
|
308
|
+
const osPath = ctx.fs.processPath(target);
|
|
309
|
+
const toTarget = await ctx.fs.resolve(to, { signal });
|
|
310
|
+
const toOsPath = ctx.fs.processPath(toTarget);
|
|
311
|
+
if (await ctx.fs.stat(toTarget, signal) !== void 0) return fail("rename: destination already exists");
|
|
312
|
+
await rename(osPath, toOsPath);
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
value: { path: toOsPath }
|
|
316
|
+
};
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return fail(mapError(error));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function deleteEntry(ctx, payload, signal) {
|
|
322
|
+
const path = pathOf(payload);
|
|
323
|
+
if (path === void 0) return fail("delete: payload.path must be a non-empty string");
|
|
324
|
+
try {
|
|
325
|
+
const target = await ctx.fs.resolve(path, { signal });
|
|
326
|
+
const osPath = ctx.fs.processPath(target);
|
|
327
|
+
const info = await ctx.fs.stat(target, signal);
|
|
328
|
+
if (info === void 0) return fail(`path not found: ${path}`);
|
|
329
|
+
await rm(osPath, {
|
|
330
|
+
recursive: info.type === "directory",
|
|
331
|
+
force: true
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
value: { path: osPath }
|
|
336
|
+
};
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return fail(mapError(error));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
178
341
|
//#endregion
|
|
179
|
-
export { CHANNEL, MAX_PREVIEW_BYTES, apply, inject, kindOf, mapDirEntry, mapError, name, sortEntries };
|
|
342
|
+
export { CHANNEL, MAX_IMAGE_BYTES, MAX_PREVIEW_BYTES, RAW_PREFIX, apply, imageMimeOf, inject, isImagePath, kindOf, mapDirEntry, mapError, name, sortEntries };
|
package/package.json
CHANGED