saltcorn-samba 0.3.0

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/index.js ADDED
@@ -0,0 +1,622 @@
1
+ /**
2
+ * saltcorn-samba
3
+ * ----------------------------------------------------------------------------
4
+ * Saltcorn plugin providing browser-based access to a Samba/CIFS share.
5
+ *
6
+ * Read paths:
7
+ * GET /sambadir – list a directory as JSON
8
+ * GET /sambafile – stream a file (inline or attachment)
9
+ * GET /sambalink – HTML page with an smb:// link
10
+ *
11
+ * Write paths (v0.3.0, opt-in via plugin config):
12
+ * POST /sambaupload – multipart upload; field name: "file" (multiple)
13
+ * POST /sambadelete – delete a file or (empty) directory
14
+ * POST /sambarename – rename or move a file / directory
15
+ * POST /sambamkdir – create a new directory
16
+ *
17
+ * All write routes require the caller's role_id <= min_role_write and a
18
+ * valid CSRF token (Saltcorn injects `req.csrfToken()`). Filenames and
19
+ * paths are validated against traversal, drive letters, UNC, control
20
+ * characters, reserved device names, and per-extension blocklists.
21
+ * ----------------------------------------------------------------------------
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const Workflow = require("@saltcorn/data/models/workflow");
27
+ const Form = require("@saltcorn/data/models/form");
28
+ const Field = require("@saltcorn/data/models/field");
29
+ const { getState } = require("@saltcorn/data/db/state");
30
+
31
+ const pkg = require("./package.json");
32
+ const {
33
+ withClient,
34
+ toSmbUrl,
35
+ mimeFromName,
36
+ sanitizeRelativePath,
37
+ sanitizeFilename,
38
+ } = require("./smb-client");
39
+ const treeView = require("./tree-view");
40
+ const fileManagerView = require("./filemanager-view");
41
+ const { samba_pdf } = require("./pdf-view");
42
+
43
+ const PLUGIN_VERSION = pkg.version;
44
+ const PLUGIN_NAME = "saltcorn-samba@" + PLUGIN_VERSION;
45
+
46
+ // Extensions blocked by default for upload. Users can override in config.
47
+ const DEFAULT_DENIED_EXT = [
48
+ "exe", "bat", "cmd", "com", "msi", "scr", "vbs", "js", "jse",
49
+ "wsf", "wsh", "ps1", "ps1xml", "psm1", "sh", "bash", "zsh",
50
+ ];
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Plugin configuration
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const configuration_workflow = () =>
57
+ new Workflow({
58
+ steps: [
59
+ {
60
+ name: "Samba server",
61
+ form: () =>
62
+ new Form({
63
+ fields: [
64
+ new Field({
65
+ name: "server",
66
+ label: "Server",
67
+ sublabel: "Hostname or IP of the Samba server (no smb:// prefix)",
68
+ type: "String",
69
+ required: true,
70
+ }),
71
+ new Field({
72
+ name: "share",
73
+ label: "Share name",
74
+ sublabel: "The share to connect to (without slashes)",
75
+ type: "String",
76
+ required: true,
77
+ }),
78
+ new Field({ name: "domain", label: "Domain / Workgroup", type: "String", default: "WORKGROUP" }),
79
+ new Field({ name: "username", label: "Username", type: "String" }),
80
+ new Field({ name: "password", label: "Password", type: "String", input_type: "password" }),
81
+ new Field({
82
+ name: "base_path",
83
+ label: "Base path",
84
+ sublabel: "Optional. All access is restricted to this sub-directory of the share.",
85
+ type: "String",
86
+ }),
87
+ new Field({ name: "port", label: "Port", type: "Integer", default: 445 }),
88
+ ],
89
+ }),
90
+ },
91
+ {
92
+ name: "Access & permissions",
93
+ form: () =>
94
+ new Form({
95
+ fields: [
96
+ new Field({
97
+ name: "min_role_read",
98
+ label: "Minimum role to read files",
99
+ sublabel: "Saltcorn role level. 1=admin, 40=staff, 80=user, 100=public. Default: 80.",
100
+ type: "Integer",
101
+ default: 80,
102
+ }),
103
+ new Field({
104
+ name: "min_role_write",
105
+ label: "Minimum role to upload / delete / rename",
106
+ sublabel:
107
+ "Set to 100 to disable all write actions completely. Default: 40 (staff and admins).",
108
+ type: "Integer",
109
+ default: 40,
110
+ }),
111
+ new Field({
112
+ name: "allow_upload",
113
+ label: "Allow file upload",
114
+ type: "Bool",
115
+ default: false,
116
+ }),
117
+ new Field({
118
+ name: "allow_delete",
119
+ label: "Allow delete",
120
+ type: "Bool",
121
+ default: false,
122
+ }),
123
+ new Field({
124
+ name: "allow_rename",
125
+ label: "Allow rename / move",
126
+ type: "Bool",
127
+ default: false,
128
+ }),
129
+ new Field({
130
+ name: "allow_mkdir",
131
+ label: "Allow creating new directories",
132
+ type: "Bool",
133
+ default: false,
134
+ }),
135
+ new Field({
136
+ name: "max_upload_mb",
137
+ label: "Max upload size per file (MiB)",
138
+ type: "Integer",
139
+ default: 50,
140
+ }),
141
+ new Field({
142
+ name: "denied_extensions",
143
+ label: "Blocked file extensions (comma-separated, no dots)",
144
+ sublabel:
145
+ "Default: exe,bat,cmd,com,msi,scr,vbs,js,jse,wsf,wsh,ps1,ps1xml,psm1,sh,bash,zsh. Leave empty for the default set.",
146
+ type: "String",
147
+ }),
148
+ new Field({
149
+ name: "public_smb_host",
150
+ label: "SMB host visible to clients",
151
+ sublabel:
152
+ "Optional. Host used in generated smb:// links. Defaults to the server field. Useful when Saltcorn runs in Docker but clients should see the LAN name.",
153
+ type: "String",
154
+ }),
155
+ ],
156
+ }),
157
+ },
158
+ ],
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Route helpers
163
+ // ---------------------------------------------------------------------------
164
+
165
+ function getConfig() {
166
+ const state = getState();
167
+ const cfgs = (state && state.plugin_cfgs) || {};
168
+ return (
169
+ cfgs[PLUGIN_NAME] ||
170
+ cfgs["saltcorn-samba"] ||
171
+ cfgs["@saltcorn/saltcorn-samba"] ||
172
+ {}
173
+ );
174
+ }
175
+
176
+ function roleOf(req) {
177
+ return (req && req.user && req.user.role_id) || 100;
178
+ }
179
+
180
+ function canRead(req, cfg) {
181
+ return roleOf(req) <= Number(cfg.min_role_read || 80);
182
+ }
183
+
184
+ function canWrite(req, cfg) {
185
+ const min = Number(cfg.min_role_write || 40);
186
+ // 100 explicitly disables all writes
187
+ if (min >= 100) return false;
188
+ return roleOf(req) <= min;
189
+ }
190
+
191
+ function jsonError(res, status, msg) {
192
+ res.status(status).json({ error: msg });
193
+ }
194
+
195
+ function jsonOk(res, extra) {
196
+ res.json({ ok: true, ...(extra || {}) });
197
+ }
198
+
199
+ /**
200
+ * Validate that the request carries a CSRF token matching the session.
201
+ * Saltcorn injects `req.csrfToken()` when the CSRF middleware is active;
202
+ * we accept either the `_csrf` body field or the `x-csrf-token` header.
203
+ */
204
+ function checkCsrf(req, res) {
205
+ if (typeof req.csrfToken !== "function") return true; // CSRF disabled globally
206
+ const expected = req.csrfToken();
207
+ const provided =
208
+ (req.body && req.body._csrf) ||
209
+ req.headers["x-csrf-token"] ||
210
+ req.headers["csrf-token"] ||
211
+ req.query._csrf;
212
+ if (!provided || provided !== expected) {
213
+ jsonError(res, 403, "Invalid CSRF token");
214
+ return false;
215
+ }
216
+ return true;
217
+ }
218
+
219
+ function deniedExtensionsFor(cfg) {
220
+ const raw = String(cfg.denied_extensions || "").trim();
221
+ if (!raw) return new Set(DEFAULT_DENIED_EXT);
222
+ return new Set(
223
+ raw
224
+ .split(/[,;\s]+/)
225
+ .map((s) => s.trim().toLowerCase().replace(/^\./, ""))
226
+ .filter(Boolean)
227
+ );
228
+ }
229
+
230
+ function checkExtensionAllowed(name, cfg) {
231
+ const denied = deniedExtensionsFor(cfg);
232
+ const dot = name.lastIndexOf(".");
233
+ const ext = dot >= 0 ? name.slice(dot + 1).toLowerCase() : "";
234
+ if (ext && denied.has(ext)) {
235
+ throw new Error("File extension '." + ext + "' is not allowed");
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Join a sanitised parent directory with a sanitised filename into a
241
+ * relative path. Both parts must already have been passed through the
242
+ * matching sanitizers.
243
+ */
244
+ function joinRel(dir, name) {
245
+ return dir ? dir + "/" + name : name;
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Read routes
250
+ // ---------------------------------------------------------------------------
251
+
252
+ const routes = [
253
+ {
254
+ url: "/sambadir",
255
+ method: "get",
256
+ callback: async ({ req, res }) => {
257
+ const cfg = getConfig();
258
+ if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
259
+ if (!canRead(req, cfg)) return jsonError(res, 403, "Forbidden");
260
+ let rel = "";
261
+ try {
262
+ rel = sanitizeRelativePath(req.query.path || "");
263
+ } catch (e) {
264
+ return jsonError(res, 400, e.message);
265
+ }
266
+ const showHidden = req.query.show_hidden === "1";
267
+ try {
268
+ const entries = await withClient(cfg, (c) => c.readdir(rel));
269
+ const items = entries
270
+ .filter((e) => showHidden || !String(e.name).startsWith("."))
271
+ .map((e) => ({
272
+ name: e.name,
273
+ isDir:
274
+ typeof e.isDirectory === "function"
275
+ ? e.isDirectory()
276
+ : !!e.isDirectory,
277
+ size: e.size || 0,
278
+ mtime: e.mtime,
279
+ birthtime: e.birthtime,
280
+ }));
281
+ // Advertise write permission to the client so it can hide buttons
282
+ // for users that lack the role.
283
+ res.json({
284
+ path: rel,
285
+ items,
286
+ perms: {
287
+ canWrite: canWrite(req, cfg),
288
+ allowUpload: cfg.allow_upload !== false && canWrite(req, cfg),
289
+ allowDelete: cfg.allow_delete !== false && canWrite(req, cfg),
290
+ allowRename: cfg.allow_rename !== false && canWrite(req, cfg),
291
+ allowMkdir: cfg.allow_mkdir !== false && canWrite(req, cfg),
292
+ maxUploadMb: Number(cfg.max_upload_mb || 50),
293
+ },
294
+ });
295
+ } catch (e) {
296
+ jsonError(res, 500, "Samba: " + (e.message || String(e)));
297
+ }
298
+ },
299
+ },
300
+
301
+ {
302
+ url: "/sambafile",
303
+ method: "get",
304
+ callback: async ({ req, res }) => {
305
+ const cfg = getConfig();
306
+ if (!cfg.server) return res.status(500).send("Samba plugin not configured");
307
+ if (!canRead(req, cfg)) return res.status(403).send("Forbidden");
308
+ let rel = "";
309
+ try {
310
+ rel = sanitizeRelativePath(req.query.path || "");
311
+ } catch (e) {
312
+ return res.status(400).send(e.message);
313
+ }
314
+ if (!rel) return res.status(400).send("path required");
315
+ const disposition =
316
+ req.query.disposition === "attachment" ? "attachment" : "inline";
317
+ const base = rel.split("/").pop();
318
+ try {
319
+ const data = await withClient(cfg, (c) => c.readFile(rel));
320
+ res.setHeader("Content-Type", mimeFromName(base));
321
+ res.setHeader("Content-Length", data.length);
322
+ res.setHeader(
323
+ "Content-Disposition",
324
+ `${disposition}; filename="${encodeURIComponent(base)}"`
325
+ );
326
+ res.setHeader("Cache-Control", "private, max-age=0, no-store");
327
+ res.end(data);
328
+ } catch (e) {
329
+ res.status(500).send("Samba: " + (e.message || String(e)));
330
+ }
331
+ },
332
+ },
333
+
334
+ {
335
+ url: "/sambalink",
336
+ method: "get",
337
+ callback: async ({ req, res }) => {
338
+ const cfg = getConfig();
339
+ if (!cfg.server) return res.status(500).send("Samba plugin not configured");
340
+ if (!canRead(req, cfg)) return res.status(403).send("Forbidden");
341
+ let rel = "";
342
+ try {
343
+ rel = sanitizeRelativePath(req.query.path || "");
344
+ } catch (e) {
345
+ return res.status(400).send(e.message);
346
+ }
347
+ const effectiveCfg = { ...cfg, server: cfg.public_smb_host || cfg.server };
348
+ const url = toSmbUrl(effectiveCfg, rel);
349
+ const esc = (s) =>
350
+ String(s).replace(/[<>&"']/g, (c) =>
351
+ ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;", "'": "&#39;" }[c])
352
+ );
353
+ const escRel = esc(rel || "/");
354
+ const escUrl = esc(url);
355
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
356
+ res.end(`<!doctype html><html><head><meta charset="utf-8">
357
+ <title>Open in file manager</title>
358
+ <style>body{font-family:system-ui,sans-serif;padding:2rem;max-width:640px;margin:auto}
359
+ a.btn{display:inline-block;padding:.6rem 1rem;background:#0d6efd;color:#fff;border-radius:6px;text-decoration:none}
360
+ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}</style>
361
+ </head><body>
362
+ <h2>Open in file manager</h2>
363
+ <p>Click below to open this location in Nemo, Nautilus, Dolphin or Windows Explorer.</p>
364
+ <p><a class="btn" href="${escUrl}">Open ${escRel}</a></p>
365
+ <p style="margin-top:2rem;color:#666">Link: <code>${escUrl}</code></p>
366
+ <p style="color:#666"><small>Some browsers require you to allow the <code>smb://</code> protocol for this site.</small></p>
367
+ </body></html>`);
368
+ },
369
+ },
370
+
371
+ // ---- Write routes (v0.3.0) --------------------------------------------
372
+
373
+ {
374
+ url: "/sambaupload",
375
+ method: "post",
376
+ callback: async ({ req, res }) => {
377
+ const cfg = getConfig();
378
+ if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
379
+ if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
380
+ if (!cfg.allow_upload) return jsonError(res, 403, "Uploads disabled");
381
+ if (!checkCsrf(req, res)) return;
382
+
383
+ // Saltcorn uses express-fileupload — files land on req.files.
384
+ const raw = req.files && (req.files.file || req.files.files);
385
+ if (!raw) return jsonError(res, 400, "No files uploaded (field 'file')");
386
+ const files = Array.isArray(raw) ? raw : [raw];
387
+ if (!files.length) return jsonError(res, 400, "No files uploaded");
388
+
389
+ let dir = "";
390
+ try {
391
+ dir = sanitizeRelativePath(req.body && req.body.path);
392
+ } catch (e) {
393
+ return jsonError(res, 400, e.message);
394
+ }
395
+
396
+ const maxBytes = Number(cfg.max_upload_mb || 50) * 1024 * 1024;
397
+ const results = [];
398
+ const overwrite = String(req.body && req.body.overwrite) === "1";
399
+
400
+ try {
401
+ await withClient(cfg, async (c) => {
402
+ for (const f of files) {
403
+ let name;
404
+ try {
405
+ name = sanitizeFilename(f.name);
406
+ checkExtensionAllowed(name, cfg);
407
+ } catch (e) {
408
+ results.push({ name: f.name, ok: false, error: e.message });
409
+ continue;
410
+ }
411
+ if (f.size > maxBytes) {
412
+ results.push({
413
+ name,
414
+ ok: false,
415
+ error: `File exceeds max size of ${cfg.max_upload_mb} MiB`,
416
+ });
417
+ continue;
418
+ }
419
+ const rel = joinRel(dir, name);
420
+ if (!overwrite) {
421
+ const exists = await c.exists(rel);
422
+ if (exists) {
423
+ results.push({ name, ok: false, error: "File already exists" });
424
+ continue;
425
+ }
426
+ }
427
+ try {
428
+ await c.writeFile(rel, f.data);
429
+ results.push({ name, ok: true, size: f.size });
430
+ } catch (e) {
431
+ results.push({ name, ok: false, error: e.message || String(e) });
432
+ }
433
+ }
434
+ });
435
+ } catch (e) {
436
+ return jsonError(res, 500, "Samba: " + (e.message || String(e)));
437
+ }
438
+
439
+ const anyFailed = results.some((r) => !r.ok);
440
+ res.status(anyFailed ? 207 : 200).json({ ok: !anyFailed, results });
441
+ },
442
+ },
443
+
444
+ {
445
+ url: "/sambadelete",
446
+ method: "post",
447
+ callback: async ({ req, res }) => {
448
+ const cfg = getConfig();
449
+ if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
450
+ if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
451
+ if (!cfg.allow_delete) return jsonError(res, 403, "Delete disabled");
452
+ if (!checkCsrf(req, res)) return;
453
+
454
+ let rel = "";
455
+ try {
456
+ rel = sanitizeRelativePath(req.body && req.body.path);
457
+ } catch (e) {
458
+ return jsonError(res, 400, e.message);
459
+ }
460
+ if (!rel) return jsonError(res, 400, "path required");
461
+
462
+ const isDir = String(req.body && req.body.isDir) === "1";
463
+ try {
464
+ await withClient(cfg, async (c) => {
465
+ if (isDir) await c.rmdir(rel);
466
+ else await c.unlink(rel);
467
+ });
468
+ jsonOk(res, { deleted: rel });
469
+ } catch (e) {
470
+ jsonError(res, 500, "Samba: " + (e.message || String(e)));
471
+ }
472
+ },
473
+ },
474
+
475
+ {
476
+ url: "/sambarename",
477
+ method: "post",
478
+ callback: async ({ req, res }) => {
479
+ const cfg = getConfig();
480
+ if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
481
+ if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
482
+ if (!cfg.allow_rename) return jsonError(res, 403, "Rename disabled");
483
+ if (!checkCsrf(req, res)) return;
484
+
485
+ let fromRel, toRel;
486
+ try {
487
+ fromRel = sanitizeRelativePath(req.body && req.body.from);
488
+ } catch (e) {
489
+ return jsonError(res, 400, "from: " + e.message);
490
+ }
491
+ if (!fromRel) return jsonError(res, 400, "from required");
492
+
493
+ // Rename accepts either a new full path OR a new bare filename in the
494
+ // same directory as `from`.
495
+ const newName = req.body && req.body.newName;
496
+ const newPath = req.body && req.body.to;
497
+ try {
498
+ if (newPath !== undefined && newPath !== null && newPath !== "") {
499
+ toRel = sanitizeRelativePath(newPath);
500
+ // The last segment must still be a valid filename.
501
+ const lastSlash = toRel.lastIndexOf("/");
502
+ const last = lastSlash >= 0 ? toRel.slice(lastSlash + 1) : toRel;
503
+ sanitizeFilename(last);
504
+ } else if (newName) {
505
+ const cleanName = sanitizeFilename(newName);
506
+ const parent = fromRel.includes("/")
507
+ ? fromRel.slice(0, fromRel.lastIndexOf("/"))
508
+ : "";
509
+ toRel = joinRel(parent, cleanName);
510
+ } else {
511
+ return jsonError(res, 400, "newName or to required");
512
+ }
513
+ } catch (e) {
514
+ return jsonError(res, 400, e.message);
515
+ }
516
+ if (fromRel === toRel) return jsonOk(res, { renamed: toRel });
517
+
518
+ // Enforce extension policy also on rename target.
519
+ try {
520
+ checkExtensionAllowed(toRel.split("/").pop(), cfg);
521
+ } catch (e) {
522
+ return jsonError(res, 400, e.message);
523
+ }
524
+
525
+ try {
526
+ await withClient(cfg, async (c) => {
527
+ const exists = await c.exists(toRel);
528
+ if (exists) throw new Error("Target already exists");
529
+ await c.rename(fromRel, toRel);
530
+ });
531
+ jsonOk(res, { from: fromRel, to: toRel });
532
+ } catch (e) {
533
+ jsonError(res, 500, "Samba: " + (e.message || String(e)));
534
+ }
535
+ },
536
+ },
537
+
538
+ {
539
+ url: "/sambamkdir",
540
+ method: "post",
541
+ callback: async ({ req, res }) => {
542
+ const cfg = getConfig();
543
+ if (!cfg.server) return jsonError(res, 500, "Samba plugin not configured");
544
+ if (!canWrite(req, cfg)) return jsonError(res, 403, "Forbidden");
545
+ if (!cfg.allow_mkdir) return jsonError(res, 403, "Mkdir disabled");
546
+ if (!checkCsrf(req, res)) return;
547
+
548
+ let parent = "";
549
+ try {
550
+ parent = sanitizeRelativePath(req.body && req.body.path);
551
+ } catch (e) {
552
+ return jsonError(res, 400, "path: " + e.message);
553
+ }
554
+ let name;
555
+ try {
556
+ name = sanitizeFilename(req.body && req.body.name);
557
+ } catch (e) {
558
+ return jsonError(res, 400, "name: " + e.message);
559
+ }
560
+ const rel = joinRel(parent, name);
561
+ try {
562
+ await withClient(cfg, async (c) => {
563
+ const exists = await c.exists(rel);
564
+ if (exists) throw new Error("Directory already exists");
565
+ await c.mkdir(rel);
566
+ });
567
+ jsonOk(res, { created: rel });
568
+ } catch (e) {
569
+ jsonError(res, 500, "Samba: " + (e.message || String(e)));
570
+ }
571
+ },
572
+ },
573
+ ];
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // Inject plugin version into view templates
577
+ // ---------------------------------------------------------------------------
578
+
579
+ function wrapView(v) {
580
+ const orig = v.run;
581
+ const origMany = v.runMany;
582
+ return {
583
+ ...v,
584
+ run: async (table_id, viewname, cfg, state, extra) =>
585
+ orig(
586
+ table_id,
587
+ viewname,
588
+ { ...cfg, __pluginVersion: PLUGIN_VERSION },
589
+ state,
590
+ extra
591
+ ),
592
+ runMany: origMany
593
+ ? async (table_id, viewname, cfg, state, extra) =>
594
+ origMany(
595
+ table_id,
596
+ viewname,
597
+ { ...cfg, __pluginVersion: PLUGIN_VERSION },
598
+ state,
599
+ extra
600
+ )
601
+ : undefined,
602
+ };
603
+ }
604
+
605
+ // ---------------------------------------------------------------------------
606
+ // Manifest
607
+ // ---------------------------------------------------------------------------
608
+
609
+ module.exports = {
610
+ sc_plugin_api_version: 1,
611
+ plugin_name: PLUGIN_NAME,
612
+ configuration_workflow,
613
+ viewtemplates: [wrapView(fileManagerView), wrapView(treeView)],
614
+ fieldviews: {
615
+ samba_pdf,
616
+ },
617
+ routes,
618
+ headers: [
619
+ { css: `/plugins/public/${PLUGIN_NAME}/samba.css` },
620
+ ],
621
+ dependencies: [],
622
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "saltcorn-samba",
3
+ "version": "0.3.0",
4
+ "description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share. File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test/sanitize.test.js",
8
+ "lint": "node --check index.js && node --check smb-client.js && node --check tree-view.js && node --check filemanager-view.js && node --check pdf-view.js && node --check public/samba-tree.js && node --check public/samba-filemanager.js",
9
+ "prepublishOnly": "npm run lint && npm test"
10
+ },
11
+ "keywords": [
12
+ "saltcorn",
13
+ "saltcorn-plugin",
14
+ "samba",
15
+ "smb",
16
+ "cifs",
17
+ "pdf",
18
+ "file-manager",
19
+ "files",
20
+ "upload",
21
+ "no-code"
22
+ ],
23
+ "author": "Peter Vassen <ich@peter-vassen.de>",
24
+ "license": "MIT",
25
+ "homepage": "https://github.com/pv-host/saltcorn-samba#readme",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/pv-host/saltcorn-samba.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/pv-host/saltcorn-samba/issues"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "@marsaud/smb2": "^0.18.0"
38
+ },
39
+ "files": [
40
+ "index.js",
41
+ "smb-client.js",
42
+ "tree-view.js",
43
+ "filemanager-view.js",
44
+ "pdf-view.js",
45
+ "public/",
46
+ "README.md",
47
+ "CHANGELOG.md",
48
+ "LICENSE"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "directories": {
54
+ "test": "test"
55
+ }
56
+ }