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/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 = ["fs", "connection"];
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-workbench",
3
3
  "description": "VS Code-style workspace file explorer + editable preview for the dsh web GUI",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",
7
7
  "engines": {