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.
@@ -0,0 +1,736 @@
1
+ /**
2
+ * Client-side controller for the SambaFileManager view (v0.3.0).
3
+ *
4
+ * Adds upload, rename, delete and mkdir on top of the read-only browser.
5
+ * Every write action goes through /sambaupload | /sambadelete | /sambarename
6
+ * | /sambamkdir with a CSRF token (rendered into the shell) and expects
7
+ * JSON responses.
8
+ */
9
+ (function () {
10
+ "use strict";
11
+
12
+ // ---- tiny DOM helper ----------------------------------------------------
13
+ function h(tag, attrs, children) {
14
+ var el = document.createElement(tag);
15
+ if (attrs)
16
+ Object.keys(attrs).forEach(function (k) {
17
+ if (k === "class") el.className = attrs[k];
18
+ else if (k === "text") el.textContent = attrs[k];
19
+ else if (k === "html") el.innerHTML = attrs[k];
20
+ else if (k.slice(0, 2) === "on")
21
+ el.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
22
+ else el.setAttribute(k, attrs[k]);
23
+ });
24
+ (children || []).forEach(function (c) {
25
+ if (c == null) return;
26
+ el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
27
+ });
28
+ return el;
29
+ }
30
+
31
+ // ---- helpers ------------------------------------------------------------
32
+ function extOf(name) {
33
+ var s = String(name || "");
34
+ var d = s.lastIndexOf(".");
35
+ return d >= 0 ? s.slice(d + 1).toLowerCase() : "";
36
+ }
37
+ function iconFor(item) {
38
+ if (item.isDir) return "📁";
39
+ var e = extOf(item.name);
40
+ if (e === "pdf") return "📄";
41
+ if (["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"].indexOf(e) >= 0) return "🖼️";
42
+ if (["doc", "docx", "odt", "rtf", "txt", "md"].indexOf(e) >= 0) return "📝";
43
+ if (["xls", "xlsx", "ods", "csv"].indexOf(e) >= 0) return "📊";
44
+ if (["ppt", "pptx", "odp"].indexOf(e) >= 0) return "📽️";
45
+ if (["zip", "tar", "gz", "7z", "rar"].indexOf(e) >= 0) return "🗜️";
46
+ if (["mp3", "wav", "ogg", "flac", "m4a"].indexOf(e) >= 0) return "🎵";
47
+ if (["mp4", "mkv", "mov", "avi", "webm"].indexOf(e) >= 0) return "🎬";
48
+ return "📎";
49
+ }
50
+ function mediaTypeFor(item) {
51
+ if (item.isDir) return "folder";
52
+ var e = extOf(item.name);
53
+ var map = {
54
+ pdf: "application/pdf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
55
+ gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
56
+ txt: "text/plain", md: "text/markdown", csv: "text/csv",
57
+ json: "application/json", xml: "application/xml", html: "text/html", htm: "text/html",
58
+ zip: "application/zip", doc: "application/msword",
59
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
60
+ xls: "application/vnd.ms-excel",
61
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
62
+ ppt: "application/vnd.ms-powerpoint",
63
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
64
+ mp3: "audio/mpeg", mp4: "video/mp4", mkv: "video/x-matroska",
65
+ mov: "video/quicktime", avi: "video/x-msvideo",
66
+ };
67
+ return map[e] || (e ? "application/" + e : "application/octet-stream");
68
+ }
69
+ function fmtSize(n) {
70
+ if (!n || n < 0) return "";
71
+ if (n < 1024) return n + " B";
72
+ var kib = n / 1024;
73
+ if (kib < 1024) return kib.toFixed(kib < 10 ? 1 : 0) + " KiB";
74
+ var mib = kib / 1024;
75
+ if (mib < 1024) return mib.toFixed(mib < 10 ? 1 : 0) + " MiB";
76
+ return (mib / 1024).toFixed(2) + " GiB";
77
+ }
78
+ function fmtDate(v) {
79
+ if (!v) return "";
80
+ var d = new Date(v);
81
+ if (isNaN(d.getTime())) return "";
82
+ var pad = function (x) { return String(x).padStart(2, "0"); };
83
+ return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) +
84
+ " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
85
+ }
86
+ function joinPath(a, b) {
87
+ if (!a) return b;
88
+ if (!b) return a;
89
+ return (a.replace(/\/+$/, "") + "/" + b.replace(/^\/+/, "")).replace(/\/+/g, "/");
90
+ }
91
+ function parentOf(p) {
92
+ if (!p) return "";
93
+ var i = p.lastIndexOf("/");
94
+ return i < 0 ? "" : p.slice(0, i);
95
+ }
96
+ function isViewable(name) {
97
+ var n = (name || "").toLowerCase();
98
+ return n.endsWith(".pdf") ||
99
+ /\.(png|jpe?g|gif|webp|svg|bmp)$/.test(n) ||
100
+ /\.(txt|md|json|xml|csv|html?)$/.test(n);
101
+ }
102
+
103
+ // ---- http helpers -------------------------------------------------------
104
+ async function fetchDir(path, showHidden) {
105
+ var url = "/sambadir?path=" + encodeURIComponent(path || "") +
106
+ (showHidden ? "&show_hidden=1" : "");
107
+ var r = await fetch(url, { credentials: "same-origin" });
108
+ if (!r.ok) {
109
+ var msg;
110
+ try { msg = (await r.json()).error; } catch (_) { msg = "HTTP " + r.status; }
111
+ throw new Error(msg || "HTTP " + r.status);
112
+ }
113
+ return r.json();
114
+ }
115
+ async function postJson(url, body, csrf) {
116
+ var r = await fetch(url, {
117
+ method: "POST",
118
+ credentials: "same-origin",
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ "X-CSRF-Token": csrf || "",
122
+ },
123
+ body: JSON.stringify(body || {}),
124
+ });
125
+ var data = null;
126
+ try { data = await r.json(); } catch (_) { data = null; }
127
+ if (!r.ok) {
128
+ var msg = (data && data.error) || ("HTTP " + r.status);
129
+ var err = new Error(msg);
130
+ err.status = r.status;
131
+ err.body = data;
132
+ throw err;
133
+ }
134
+ return data || { ok: true };
135
+ }
136
+
137
+ // ---- modal & toast ------------------------------------------------------
138
+ function toast(msg, type) {
139
+ var box = h("div", {
140
+ class: "samba-toast samba-toast-" + (type || "info"),
141
+ text: msg,
142
+ }, []);
143
+ document.body.appendChild(box);
144
+ setTimeout(function () { box.classList.add("samba-toast-hide"); }, 3200);
145
+ setTimeout(function () { if (box.parentNode) box.parentNode.removeChild(box); }, 3800);
146
+ }
147
+ function modal(title, bodyEl, footerEls) {
148
+ var backdrop = h("div", { class: "samba-modal-backdrop" }, []);
149
+ var modalEl = h("div", { class: "samba-modal card" }, [
150
+ h("div", { class: "samba-modal-header card-header d-flex align-items-center" }, [
151
+ h("strong", { text: title }, []),
152
+ h("button", {
153
+ type: "button", class: "btn-close ms-auto",
154
+ "aria-label": "Close",
155
+ onclick: function () { close(); },
156
+ }, []),
157
+ ]),
158
+ h("div", { class: "samba-modal-body card-body" }, [bodyEl]),
159
+ h("div", { class: "samba-modal-footer card-footer d-flex justify-content-end gap-2" }, footerEls || []),
160
+ ]);
161
+ backdrop.appendChild(modalEl);
162
+ document.body.appendChild(backdrop);
163
+ function close() {
164
+ if (backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
165
+ document.removeEventListener("keydown", onKey);
166
+ }
167
+ function onKey(e) { if (e.key === "Escape") close(); }
168
+ document.addEventListener("keydown", onKey);
169
+ backdrop.addEventListener("click", function (e) {
170
+ if (e.target === backdrop) close();
171
+ });
172
+ return { close: close, root: modalEl };
173
+ }
174
+
175
+ // ---- component ----------------------------------------------------------
176
+ function mount(id, opts) {
177
+ opts = opts || {};
178
+ var root = document.getElementById(id + "-list");
179
+ if (!root) return;
180
+ if (root.dataset.mounted === "1") return;
181
+ root.dataset.mounted = "1";
182
+
183
+ var toolbar = document.getElementById(id + "-toolbar");
184
+ var counter = document.getElementById(id + "-count");
185
+ var viewer = document.getElementById(id + "-viewer");
186
+
187
+ var state = {
188
+ path: opts.startPath || "",
189
+ rootPath: opts.startPath || "",
190
+ showHidden: !!opts.showHidden,
191
+ sortBy: "name",
192
+ sortDir: 1,
193
+ page: 1,
194
+ pageSize: Number(opts.pageSize) || 0,
195
+ lastItems: [],
196
+ perms: {},
197
+ };
198
+
199
+ function canGoUp() {
200
+ if (!opts.allowNavigateUp) return false;
201
+ return state.path.length > state.rootPath.length;
202
+ }
203
+
204
+ // ---- toolbar --------------------------------------------------------
205
+ function renderToolbar() {
206
+ toolbar.innerHTML = "";
207
+ var upBtn = h("button", {
208
+ type: "button",
209
+ class: "btn btn-sm btn-outline-secondary me-1" + (canGoUp() ? "" : " disabled"),
210
+ onclick: function () { if (canGoUp()) navigate(parentOf(state.path)); },
211
+ title: "Up one directory",
212
+ }, ["⬆ Up"]);
213
+ var homeBtn = h("button", {
214
+ type: "button",
215
+ class: "btn btn-sm btn-outline-secondary me-1" + (state.path === state.rootPath ? " disabled" : ""),
216
+ onclick: function () { navigate(state.rootPath); },
217
+ title: "Go to root",
218
+ }, ["🏠"]);
219
+ var reloadBtn = h("button", {
220
+ type: "button",
221
+ class: "btn btn-sm btn-outline-secondary me-2",
222
+ onclick: function () { load(state.path); },
223
+ title: "Refresh",
224
+ }, ["↻"]);
225
+
226
+ var breadcrumb = h("nav", { class: "d-inline-block", "aria-label": "breadcrumb" }, [
227
+ (function () {
228
+ var ol = h("ol", { class: "breadcrumb mb-0 d-inline-flex" }, []);
229
+ var rel = state.path.slice(state.rootPath.length).replace(/^\/+/, "");
230
+ var parts = rel ? rel.split("/") : [];
231
+ ol.appendChild(h("li", {
232
+ class: "breadcrumb-item" + (parts.length ? "" : " active"),
233
+ }, [
234
+ parts.length
235
+ ? h("a", { href: "#", onclick: function (e) { e.preventDefault(); navigate(state.rootPath); } }, ["/"])
236
+ : "/",
237
+ ]));
238
+ var cur = state.rootPath;
239
+ parts.forEach(function (seg, idx) {
240
+ cur = joinPath(cur, seg);
241
+ var isLast = idx === parts.length - 1;
242
+ var target = cur;
243
+ ol.appendChild(h("li", { class: "breadcrumb-item" + (isLast ? " active" : "") }, [
244
+ isLast ? seg : h("a", {
245
+ href: "#",
246
+ onclick: function (e) { e.preventDefault(); navigate(target); },
247
+ }, [seg]),
248
+ ]));
249
+ });
250
+ return ol;
251
+ })(),
252
+ ]);
253
+
254
+ var hiddenToggle = h("label", { class: "form-check form-check-inline ms-3 mb-0 align-middle" }, [
255
+ h("input", {
256
+ type: "checkbox", class: "form-check-input",
257
+ onchange: function (e) { state.showHidden = e.target.checked; load(state.path); },
258
+ }, []),
259
+ h("span", { class: "form-check-label small ms-1", text: "Show hidden" }, []),
260
+ ]);
261
+ hiddenToggle.querySelector("input").checked = state.showHidden;
262
+
263
+ toolbar.appendChild(upBtn);
264
+ toolbar.appendChild(homeBtn);
265
+ toolbar.appendChild(reloadBtn);
266
+ toolbar.appendChild(breadcrumb);
267
+ toolbar.appendChild(hiddenToggle);
268
+
269
+ // Write buttons on the right
270
+ var right = h("span", { class: "ms-auto d-inline-flex gap-1" }, []);
271
+ if (state.perms.allowUpload) {
272
+ right.appendChild(h("button", {
273
+ type: "button",
274
+ class: "btn btn-sm btn-primary",
275
+ onclick: openUploadDialog,
276
+ title: "Upload files to this directory",
277
+ }, ["⬆ Upload"]));
278
+ }
279
+ if (state.perms.allowMkdir) {
280
+ right.appendChild(h("button", {
281
+ type: "button",
282
+ class: "btn btn-sm btn-outline-primary",
283
+ onclick: openMkdirDialog,
284
+ title: "Create new folder",
285
+ }, ["+ Folder"]));
286
+ }
287
+ if (right.childNodes.length) {
288
+ toolbar.appendChild(h("span", { class: "flex-grow-1" }, []));
289
+ toolbar.appendChild(right);
290
+ }
291
+ }
292
+
293
+ // ---- table ----------------------------------------------------------
294
+ function sortItems(items) {
295
+ var by = state.sortBy, dir = state.sortDir;
296
+ return items.slice().sort(function (a, b) {
297
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
298
+ var va, vb;
299
+ if (by === "size") { va = a.size || 0; vb = b.size || 0; }
300
+ else if (by === "mtime") {
301
+ va = a.mtime ? new Date(a.mtime).getTime() : 0;
302
+ vb = b.mtime ? new Date(b.mtime).getTime() : 0;
303
+ } else if (by === "type") { va = mediaTypeFor(a); vb = mediaTypeFor(b); }
304
+ else { va = String(a.name).toLowerCase(); vb = String(b.name).toLowerCase(); }
305
+ if (va < vb) return -1 * dir;
306
+ if (va > vb) return 1 * dir;
307
+ return 0;
308
+ });
309
+ }
310
+ function sortHeader(label, key) {
311
+ var arrow = state.sortBy === key ? (state.sortDir > 0 ? " ▲" : " ▼") : "";
312
+ return h("th", {
313
+ class: "samba-fm-th",
314
+ style: "cursor:pointer;user-select:none;",
315
+ onclick: function () {
316
+ if (state.sortBy === key) state.sortDir = -state.sortDir;
317
+ else { state.sortBy = key; state.sortDir = 1; }
318
+ render();
319
+ },
320
+ }, [label + arrow]);
321
+ }
322
+ function renderTable(items) {
323
+ root.innerHTML = "";
324
+ var showWriteActions = state.perms.allowDelete || state.perms.allowRename;
325
+ var table = h("table", {
326
+ class: "table table-sm table-hover mb-0 samba-fm-table align-middle",
327
+ }, [
328
+ h("thead", {}, [
329
+ h("tr", {}, [
330
+ h("th", { style: "width:2.4rem;" }, []),
331
+ sortHeader("Filename", "name"),
332
+ sortHeader("Media type", "type"),
333
+ sortHeader("Size", "size"),
334
+ sortHeader("Modified", "mtime"),
335
+ h("th", { style: "width:" + (showWriteActions ? "18rem" : "14rem") + ";" }, ["Actions"]),
336
+ ]),
337
+ ]),
338
+ ]);
339
+ var tbody = h("tbody", {}, []);
340
+ var sorted = sortItems(items);
341
+ var slice = sorted;
342
+ if (state.pageSize > 0) {
343
+ var from = (state.page - 1) * state.pageSize;
344
+ slice = sorted.slice(from, from + state.pageSize);
345
+ }
346
+
347
+ slice.forEach(function (item) {
348
+ var full = joinPath(state.path, item.name);
349
+ var isDir = item.isDir;
350
+
351
+ var nameCell = h("td", {}, [
352
+ h("a", {
353
+ href: "#", class: "samba-fm-name",
354
+ onclick: function (e) {
355
+ e.preventDefault();
356
+ if (isDir) navigate(full); else openFile(item, full);
357
+ },
358
+ }, [item.name]),
359
+ ]);
360
+
361
+ var actions = [];
362
+ if (isDir) {
363
+ actions.push(h("button", {
364
+ type: "button", class: "btn btn-sm btn-outline-secondary me-1",
365
+ onclick: function () { navigate(full); },
366
+ }, ["Open"]));
367
+ } else {
368
+ actions.push(h("a", {
369
+ class: "btn btn-sm btn-outline-secondary me-1",
370
+ href: "/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline",
371
+ target: "_blank", title: "Open in new tab",
372
+ }, ["View"]));
373
+ actions.push(h("a", {
374
+ class: "btn btn-sm btn-outline-secondary me-1",
375
+ href: "/sambafile?path=" + encodeURIComponent(full) + "&disposition=attachment",
376
+ title: "Download",
377
+ }, ["⬇"]));
378
+ }
379
+ if (opts.exposeSmbLink) {
380
+ actions.push(h("a", {
381
+ class: "btn btn-sm btn-outline-primary me-1",
382
+ href: "/sambalink?path=" + encodeURIComponent(full),
383
+ target: "_blank",
384
+ title: "Open in file manager (Nemo/Nautilus/Explorer)",
385
+ }, ["↗"]));
386
+ }
387
+ if (state.perms.allowRename) {
388
+ actions.push(h("button", {
389
+ type: "button", class: "btn btn-sm btn-outline-secondary me-1",
390
+ title: "Rename",
391
+ onclick: function () { openRenameDialog(item, full); },
392
+ }, ["✎"]));
393
+ }
394
+ if (state.perms.allowDelete) {
395
+ actions.push(h("button", {
396
+ type: "button", class: "btn btn-sm btn-outline-danger",
397
+ title: "Delete",
398
+ onclick: function () { confirmDelete(item, full); },
399
+ }, ["🗑"]));
400
+ }
401
+
402
+ tbody.appendChild(h("tr", {}, [
403
+ h("td", { class: "samba-fm-icon", style: "font-size:1.2rem;" }, [iconFor(item)]),
404
+ nameCell,
405
+ h("td", { class: "text-muted small" }, [mediaTypeFor(item)]),
406
+ h("td", { class: "text-muted small" }, [isDir ? "" : fmtSize(item.size)]),
407
+ h("td", { class: "text-muted small" }, [fmtDate(item.mtime)]),
408
+ h("td", {}, actions),
409
+ ]));
410
+ });
411
+ table.appendChild(tbody);
412
+ root.appendChild(table);
413
+
414
+ if (!items.length) {
415
+ root.appendChild(h("div", {
416
+ class: "text-muted fst-italic p-3 text-center",
417
+ text: "(empty directory)",
418
+ }, []));
419
+ }
420
+
421
+ if (state.pageSize > 0 && sorted.length > state.pageSize) {
422
+ var totalPages = Math.ceil(sorted.length / state.pageSize);
423
+ root.appendChild(h("div", {
424
+ class: "d-flex align-items-center justify-content-end p-2 border-top",
425
+ }, [
426
+ h("span", { class: "text-muted small me-2", text: "Page " + state.page + " / " + totalPages }, []),
427
+ h("button", {
428
+ type: "button",
429
+ class: "btn btn-sm btn-outline-secondary me-1" + (state.page <= 1 ? " disabled" : ""),
430
+ onclick: function () { if (state.page > 1) { state.page--; render(); } },
431
+ }, ["‹"]),
432
+ h("button", {
433
+ type: "button",
434
+ class: "btn btn-sm btn-outline-secondary" + (state.page >= totalPages ? " disabled" : ""),
435
+ onclick: function () { if (state.page < totalPages) { state.page++; render(); } },
436
+ }, ["›"]),
437
+ ]));
438
+ }
439
+ }
440
+ function updateCounter(items) {
441
+ if (!counter) return;
442
+ var dirs = items.filter(function (i) { return i.isDir; }).length;
443
+ var files = items.length - dirs;
444
+ counter.textContent = dirs + " folders · " + files + " files";
445
+ }
446
+ function render() {
447
+ renderToolbar();
448
+ renderTable(state.lastItems);
449
+ updateCounter(state.lastItems);
450
+ }
451
+
452
+ // ---- navigation & viewer -------------------------------------------
453
+ function navigate(path) { state.page = 1; load(path); }
454
+ function openFile(item, full) {
455
+ if (!viewer) {
456
+ window.open("/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline", "_blank");
457
+ return;
458
+ }
459
+ if (opts.pdfInline && isViewable(item.name)) {
460
+ viewer.innerHTML = "";
461
+ var isImg = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(item.name);
462
+ var url = "/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline";
463
+ var header = h("div", {
464
+ class: "samba-fm-viewer-header d-flex align-items-center p-2 border-top",
465
+ }, [
466
+ h("strong", { text: item.name }, []),
467
+ h("span", { class: "text-muted small ms-2", text: fmtSize(item.size) }, []),
468
+ h("button", {
469
+ type: "button", class: "btn btn-sm btn-outline-secondary ms-auto",
470
+ onclick: function () { viewer.innerHTML = ""; },
471
+ }, ["Close"]),
472
+ ]);
473
+ viewer.appendChild(header);
474
+ if (isImg) {
475
+ viewer.appendChild(h("img", {
476
+ src: url,
477
+ style: "max-width:100%;max-height:70vh;display:block;margin:0 auto;",
478
+ }, []));
479
+ } else {
480
+ viewer.appendChild(h("iframe", {
481
+ src: url, style: "width:100%;height:70vh;border:0;display:block;",
482
+ }, []));
483
+ }
484
+ } else {
485
+ window.open("/sambafile?path=" + encodeURIComponent(full) + "&disposition=inline", "_blank");
486
+ }
487
+ }
488
+
489
+ // ---- write actions --------------------------------------------------
490
+ function confirmDelete(item, full) {
491
+ var body = h("div", {}, [
492
+ h("p", {}, [
493
+ "Delete ",
494
+ h("strong", { text: item.name }, []),
495
+ "?",
496
+ ]),
497
+ item.isDir
498
+ ? h("div", { class: "alert alert-warning small mb-0" }, [
499
+ "This will only succeed if the directory is empty.",
500
+ ])
501
+ : h("div", { class: "text-muted small" }, ["This action cannot be undone."]),
502
+ ]);
503
+ var m = modal("Confirm delete", body, [
504
+ h("button", {
505
+ type: "button", class: "btn btn-outline-secondary",
506
+ onclick: function () { m.close(); },
507
+ }, ["Cancel"]),
508
+ h("button", {
509
+ type: "button", class: "btn btn-danger",
510
+ onclick: async function () {
511
+ try {
512
+ await postJson("/sambadelete", {
513
+ path: full, isDir: item.isDir ? "1" : "0",
514
+ }, opts.csrfToken);
515
+ m.close();
516
+ toast("Deleted " + item.name, "success");
517
+ load(state.path);
518
+ } catch (e) {
519
+ toast("Delete failed: " + e.message, "error");
520
+ }
521
+ },
522
+ }, ["Delete"]),
523
+ ]);
524
+ }
525
+
526
+ function openRenameDialog(item, full) {
527
+ var input = h("input", {
528
+ type: "text", class: "form-control", value: item.name,
529
+ }, []);
530
+ var body = h("div", {}, [
531
+ h("label", { class: "form-label small text-muted", text: "New name" }, []),
532
+ input,
533
+ h("div", { class: "form-text small", text: "Slashes are not allowed. Move by editing folder path? Use the ↗ browse feature instead." }, []),
534
+ ]);
535
+ var m = modal("Rename '" + item.name + "'", body, [
536
+ h("button", {
537
+ type: "button", class: "btn btn-outline-secondary",
538
+ onclick: function () { m.close(); },
539
+ }, ["Cancel"]),
540
+ h("button", {
541
+ type: "button", class: "btn btn-primary",
542
+ onclick: async function () {
543
+ var v = input.value.trim();
544
+ if (!v || v === item.name) { m.close(); return; }
545
+ try {
546
+ await postJson("/sambarename", {
547
+ from: full, newName: v,
548
+ }, opts.csrfToken);
549
+ m.close();
550
+ toast("Renamed to " + v, "success");
551
+ load(state.path);
552
+ } catch (e) {
553
+ toast("Rename failed: " + e.message, "error");
554
+ }
555
+ },
556
+ }, ["Rename"]),
557
+ ]);
558
+ setTimeout(function () { input.focus(); input.select(); }, 20);
559
+ }
560
+
561
+ function openMkdirDialog() {
562
+ var input = h("input", {
563
+ type: "text", class: "form-control", placeholder: "New folder name",
564
+ }, []);
565
+ var body = h("div", {}, [
566
+ h("label", { class: "form-label small text-muted", text: "Folder name" }, []),
567
+ input,
568
+ ]);
569
+ var m = modal("Create folder in " + (state.path || "/"), body, [
570
+ h("button", {
571
+ type: "button", class: "btn btn-outline-secondary",
572
+ onclick: function () { m.close(); },
573
+ }, ["Cancel"]),
574
+ h("button", {
575
+ type: "button", class: "btn btn-primary",
576
+ onclick: async function () {
577
+ var v = input.value.trim();
578
+ if (!v) return;
579
+ try {
580
+ await postJson("/sambamkdir", {
581
+ path: state.path, name: v,
582
+ }, opts.csrfToken);
583
+ m.close();
584
+ toast("Created folder " + v, "success");
585
+ load(state.path);
586
+ } catch (e) {
587
+ toast("Create folder failed: " + e.message, "error");
588
+ }
589
+ },
590
+ }, ["Create"]),
591
+ ]);
592
+ setTimeout(function () { input.focus(); }, 20);
593
+ }
594
+
595
+ function openUploadDialog() {
596
+ var maxMb = Number(state.perms.maxUploadMb) || 50;
597
+ var fileInput = h("input", {
598
+ type: "file", class: "form-control", multiple: "multiple",
599
+ }, []);
600
+ var overwriteCb = h("input", { type: "checkbox", class: "form-check-input" }, []);
601
+ var dropZone = h("div", {
602
+ class: "samba-drop border rounded p-4 text-center text-muted",
603
+ text: "Drop files here or click to select",
604
+ onclick: function () { fileInput.click(); },
605
+ }, []);
606
+ var picked = h("div", { class: "samba-picked small mt-2" }, []);
607
+
608
+ function setFiles(fileList) {
609
+ fileInput.__files = Array.prototype.slice.call(fileList);
610
+ picked.innerHTML = "";
611
+ if (!fileInput.__files.length) return;
612
+ var ul = h("ul", { class: "list-unstyled mb-0" }, []);
613
+ fileInput.__files.forEach(function (f) {
614
+ ul.appendChild(h("li", { text: f.name + " (" + fmtSize(f.size) + ")" }, []));
615
+ });
616
+ picked.appendChild(ul);
617
+ }
618
+ fileInput.addEventListener("change", function () { setFiles(fileInput.files); });
619
+ ["dragenter", "dragover"].forEach(function (ev) {
620
+ dropZone.addEventListener(ev, function (e) {
621
+ e.preventDefault(); e.stopPropagation();
622
+ dropZone.classList.add("samba-drop-active");
623
+ });
624
+ });
625
+ ["dragleave", "drop"].forEach(function (ev) {
626
+ dropZone.addEventListener(ev, function (e) {
627
+ e.preventDefault(); e.stopPropagation();
628
+ dropZone.classList.remove("samba-drop-active");
629
+ });
630
+ });
631
+ dropZone.addEventListener("drop", function (e) {
632
+ if (e.dataTransfer && e.dataTransfer.files) setFiles(e.dataTransfer.files);
633
+ });
634
+
635
+ var progress = h("div", { class: "samba-upload-progress mt-2" }, []);
636
+ var body = h("div", {}, [
637
+ h("div", { class: "small text-muted mb-2" }, [
638
+ "Uploading to ",
639
+ h("code", { text: state.path || "/" }, []),
640
+ ". Max " + maxMb + " MiB per file.",
641
+ ]),
642
+ dropZone,
643
+ h("div", { class: "mt-2" }, [
644
+ h("label", { class: "form-label small text-muted", text: "…or pick files" }, []),
645
+ fileInput,
646
+ ]),
647
+ picked,
648
+ h("label", { class: "form-check mt-2" }, [
649
+ overwriteCb,
650
+ h("span", { class: "form-check-label ms-1 small", text: "Overwrite existing files" }, []),
651
+ ]),
652
+ progress,
653
+ ]);
654
+
655
+ var uploadBtn;
656
+ var m = modal("Upload files", body, [
657
+ h("button", {
658
+ type: "button", class: "btn btn-outline-secondary",
659
+ onclick: function () { m.close(); },
660
+ }, ["Cancel"]),
661
+ (uploadBtn = h("button", {
662
+ type: "button", class: "btn btn-primary",
663
+ onclick: async function () {
664
+ var files = fileInput.__files || Array.prototype.slice.call(fileInput.files || []);
665
+ if (!files.length) { toast("No files selected", "error"); return; }
666
+ uploadBtn.setAttribute("disabled", "disabled");
667
+ try {
668
+ var fd = new FormData();
669
+ fd.append("path", state.path);
670
+ fd.append("overwrite", overwriteCb.checked ? "1" : "0");
671
+ files.forEach(function (f) { fd.append("file", f); });
672
+ progress.innerHTML = "Uploading…";
673
+ var r = await fetch("/sambaupload", {
674
+ method: "POST",
675
+ credentials: "same-origin",
676
+ headers: { "X-CSRF-Token": opts.csrfToken || "" },
677
+ body: fd,
678
+ });
679
+ var data = null;
680
+ try { data = await r.json(); } catch (_) { data = null; }
681
+ if (!r.ok && r.status !== 207) {
682
+ throw new Error((data && data.error) || ("HTTP " + r.status));
683
+ }
684
+ var results = (data && data.results) || [];
685
+ var okCount = results.filter(function (x) { return x.ok; }).length;
686
+ var failed = results.filter(function (x) { return !x.ok; });
687
+ if (failed.length) {
688
+ progress.innerHTML = "";
689
+ progress.appendChild(h("div", {
690
+ class: "alert alert-warning small mb-0",
691
+ }, [
692
+ "Uploaded " + okCount + " / " + results.length + " files. Failures:",
693
+ h("ul", { class: "mb-0" }, failed.map(function (r) {
694
+ return h("li", { text: r.name + ": " + r.error }, []);
695
+ })),
696
+ ]));
697
+ toast("Some uploads failed", "error");
698
+ } else {
699
+ toast("Uploaded " + okCount + " file(s)", "success");
700
+ m.close();
701
+ }
702
+ load(state.path);
703
+ } catch (e) {
704
+ progress.innerHTML = "";
705
+ progress.appendChild(h("div", {
706
+ class: "alert alert-danger small mb-0", text: "Upload failed: " + e.message,
707
+ }, []));
708
+ toast("Upload failed", "error");
709
+ } finally {
710
+ uploadBtn.removeAttribute("disabled");
711
+ }
712
+ },
713
+ }, ["Upload"])),
714
+ ]);
715
+ }
716
+
717
+ // ---- data ------------------------------------------------------------
718
+ async function load(path) {
719
+ state.path = path;
720
+ root.innerHTML = '<div class="text-muted p-3">Loading…</div>';
721
+ try {
722
+ var data = await fetchDir(path, state.showHidden);
723
+ state.lastItems = data.items || [];
724
+ state.perms = data.perms || {};
725
+ render();
726
+ } catch (e) {
727
+ root.innerHTML =
728
+ '<div class="alert alert-danger m-2">Samba: ' + (e.message || String(e)) + "</div>";
729
+ renderToolbar();
730
+ }
731
+ }
732
+ load(state.path);
733
+ }
734
+
735
+ window.saltcornSambaMountFM = mount;
736
+ })();