saltcorn-samba 0.4.1 → 0.4.3

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 CHANGED
@@ -4,6 +4,77 @@ All notable changes to `saltcorn-samba` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.4.3] – 2026-07-05
8
+
9
+ ### Fixed – **Unklare Meldung bei nicht-existierendem Basispfad**
10
+
11
+ Wenn ein Basispfad angegeben wurde, der auf dem Server nicht existiert
12
+ (oder für den angemeldeten Benutzer nicht sichtbar ist), meldete die
13
+ Test-Route bisher nur die rohe Server-Antwort:
14
+
15
+ ```
16
+ CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND (ENOENT)
17
+ CREATE failed: STATUS_OBJECT_PATH_NOT_FOUND (ENOENT)
18
+ ```
19
+
20
+ Daraus konnte der Benutzer nicht erkennen, ob es sich um einen Tippfehler,
21
+ um einen Klein-/Großschreibungs-Konflikt (Samba mit `case sensitive = yes`)
22
+ oder um eine Berechtigung handelt.
23
+
24
+ **Neu:**
25
+
26
+ 1. Die Test-Route prüft den Basispfad jetzt zuerst mit `stat()`, bevor sie
27
+ `readdir()` versucht. Fehlt der Ordner, wird eine deutsche Meldung
28
+ zurückgegeben, die den betroffenen Pfad, die Freigabe und typische
29
+ Ursachen (Schreibweise, Groß-/Kleinschreibung, Zugriffsrechte) nennt.
30
+ 2. Zusätzlich liefert die Route ein `diagnostics`-Objekt zurück, das
31
+ den fehlenden Segmentnamen, den übergeordneten Pfad und — sofern der
32
+ übergeordnete Ordner auflistbar ist — dessen tatsächliche Einträge
33
+ enthält. Das Test-UI hebt ähnlich geschriebene Nachbareinträge hervor,
34
+ damit Tippfehler oder Case-Mismatch sofort sichtbar werden.
35
+ 3. Der `smb-client.js`-`readdir()`-Wrapper mappt `OBJECT_NAME_NOT_FOUND`
36
+ und `OBJECT_PATH_NOT_FOUND` ebenfalls auf eine deutsche Meldung, damit
37
+ auch der File-Manager (außerhalb der Test-Route) verständlich über
38
+ fehlende Ordner informiert.
39
+
40
+ **Kein Config-Migrationsschritt nötig.** Wer die neuen Diagnose-Boxen
41
+ sehen will, muss lediglich `pv-host/saltcorn-samba@0.4.3` einspielen.
42
+
43
+ ## [0.4.2] – 2026-07-05
44
+
45
+ ### Fixed – **Dropdown-Felder zeigen `[object Object]` statt Optionen**
46
+
47
+ Die neuen Felder `signing_mode` und `encryption_mode` wurden in der
48
+ Saltcorn-UI mit `[object Object]` als einzige Auswahl gerendert.
49
+
50
+ **Ursache:** In der genutzten Saltcorn-Version werden `attributes.options`
51
+ als einfaches String-Array erwartet (`["a", "b", "c"]`), nicht als
52
+ `{value, label}`-Objekt. Die Objekt-Form ist erst in neueren Builds
53
+ vollständig unterstützt; sonst castet das UI die Objekte zu Strings.
54
+
55
+ **Fix:** `options` auf String-Array umgestellt —
56
+ `["if-offered", "required", "disabled"]`. Die Beschriftung bleibt in
57
+ der `sublabel` erhalten (der Config-Wizard erklärt jeden Wert dort).
58
+
59
+ ### Fixed – **`STATUS_OBJECT_NAME_NOT_FOUND` durch fehlgeleiteten Root-Fallback**
60
+
61
+ Der 0.4.1-Fallback `share/.` triggerte auf Samba einen anderen NT-Status:
62
+ `STATUS_OBJECT_NAME_NOT_FOUND` (0xC0000034 / `ENOENT`), weil Samba `.` als
63
+ literalen Dateinamen sucht statt als Current-Directory-Marker (das ist im
64
+ POSIX-Layer, nicht im SMB2-Protokoll). Der zweite Fallback `share/*` ist
65
+ protokoll-illegal (Wildcard im CREATE) und wird ebenfalls abgelehnt.
66
+
67
+ **Neuer Ansatz:** Wir versuchen den Fallback erst gar nicht. Statt zu
68
+ raten geben wir eine klare deutsche Fehlermeldung aus: **einen Basispfad
69
+ setzen**. Das ist die einzige zuverlässige Lösung, solange `smb3-client`
70
+ die `FileInformationClass` nicht konfigurierbar macht.
71
+
72
+ Die Test-Route bleibt weiterhin nachsichtig: bei `OBJECT_NAME_INVALID`
73
+ auf dem Root fällt sie auf `stat("")` zurück und meldet ein Erfolg mit
74
+ Hinweis („Verbindung + Anmeldung erfolgreich, aber Share-Root nicht
75
+ direkt auflistbar — bitte Basispfad setzen“). Der Hinweis wird als gelbe
76
+ Box im Test-Ergebnis angezeigt.
77
+
7
78
  ## [0.4.1] – 2026-07-05
8
79
 
9
80
  ### Fixed – **`QUERY_DIRECTORY failed: 0xC0000033` auf Share-Root (Samba 4.20+/4.23+)**
package/index.js CHANGED
@@ -160,6 +160,9 @@ window.sambaTestConn = async function(btn) {
160
160
  var rows = (data.entries||[]).map(function(e){
161
161
  return '<li>'+ (e.isDirectory?'📁 ':'📄 ') + String(e.name).replace(/[<>&]/g,'?') +'</li>';
162
162
  }).join('');
163
+ var noteHtml = data.note
164
+ ? '<div style="margin-top:.5rem;padding:.4rem .6rem;background:#fff3cd;border:1px solid #ffeeba;border-radius:.25rem"><b>Hinweis:</b> ' + String(data.note).replace(/[<>&]/g,'?') + '</div>'
165
+ : '';
163
166
  out.innerHTML =
164
167
  '<div class="alert alert-success">' +
165
168
  '<b>✓ Verbindung erfolgreich</b> (' + data.duration_ms + ' ms)<br>' +
@@ -167,6 +170,7 @@ window.sambaTestConn = async function(btn) {
167
170
  'Basispfad: <code>' + data.base_path + '</code>, Benutzer: <code>' + data.username + '</code><br>' +
168
171
  'Einträge gefunden: <b>' + data.entry_count + '</b>' + (data.truncated ? ' (erste 20 unten)' : '') +
169
172
  (rows ? '<ul style="margin:.5rem 0 0 1rem">' + rows + '</ul>' : '') +
173
+ noteHtml +
170
174
  '</div>';
171
175
  } else {
172
176
  var a = data && data.attempted || {};
@@ -180,6 +184,40 @@ window.sambaTestConn = async function(btn) {
180
184
  'Fehler: <code>' + String(data && data.error || 'Unbekannt').replace(/[<>&]/g,'?') + '</code>' +
181
185
  (data && data.code ? ' <span class="text-muted">(' + data.code + ')</span>' : '') + '<br>' +
182
186
  (data && data.hint ? '<div style="margin-top:.4rem"><b>Hinweis:</b> ' + String(data.hint).replace(/[<>&]/g,'?') + '</div>' : '') +
187
+ ((function(){
188
+ var d = data && data.diagnostics;
189
+ if (!d) return '';
190
+ var esc = function(s){ return String(s==null?'':s).replace(/[<>&]/g,function(c){return {'<':'&lt;','>':'&gt;','&':'&amp;'}[c];}); };
191
+ var lc = String(d.missing_segment||'').toLowerCase();
192
+ var sibs = Array.isArray(d.siblings) ? d.siblings : [];
193
+ var similar = sibs.filter(function(s){
194
+ var n = String(s.name||'').toLowerCase();
195
+ if (!n || !lc) return false;
196
+ if (n === lc) return true;
197
+ if (n.indexOf(lc) !== -1 || lc.indexOf(n) !== -1) return true;
198
+ // simple Levenshtein-1 heuristic: same length ±1 and share prefix
199
+ return Math.abs(n.length - lc.length) <= 1 && n.substring(0, Math.min(3, n.length)) === lc.substring(0, Math.min(3, lc.length));
200
+ });
201
+ var box = '<div style="margin-top:.4rem;padding:.4rem .6rem;background:#f8d7da;border:1px solid #f5c2c7;border-radius:.25rem">';
202
+ box += '<b>Diagnose:</b> Der Server meldet, dass \u201e<code>' + esc(d.missing_segment) + '</code>\u201c im Ordner \u201e<code>' + esc(d.parent_path) + '</code>\u201c nicht existiert.';
203
+ if (!d.parent_listable) {
204
+ box += '<br><span class="text-muted">(Der übergeordnete Ordner konnte nicht aufgelistet werden — Share-Root-Auflistung ist mit smb3-client aktuell blockiert.)</span>';
205
+ } else if (sibs.length === 0) {
206
+ box += '<br>Der übergeordnete Ordner ist leer.';
207
+ } else {
208
+ if (similar.length) {
209
+ box += '<br><b>Ähnliche Einträge, die tatsächlich existieren:</b><ul style="margin:.3rem 0 .3rem 1rem">';
210
+ similar.slice(0, 10).forEach(function(s){ box += '<li>' + (s.isDirectory?'📁 ':'📄 ') + '<code>' + esc(s.name) + '</code></li>'; });
211
+ box += '</ul>';
212
+ }
213
+ box += '<details style="margin-top:.3rem"><summary>Alle Einträge im Ordner \u201e' + esc(d.parent_path) + '\u201c anzeigen (' + sibs.length + ')</summary><ul style="margin:.3rem 0 0 1rem">';
214
+ sibs.slice(0, 100).forEach(function(s){ box += '<li>' + (s.isDirectory?'📁 ':'📄 ') + '<code>' + esc(s.name) + '</code></li>'; });
215
+ if (sibs.length > 100) box += '<li><i>… (' + (sibs.length - 100) + ' weitere)</i></li>';
216
+ box += '</ul></details>';
217
+ }
218
+ box += '</div>';
219
+ return box;
220
+ })()) +
183
221
  (a.server ? (
184
222
  '<details style="margin-top:.4rem"><summary>Versuchte Verbindungsdaten</summary>' +
185
223
  '<table class="table table-sm" style="margin-top:.4rem">' +
@@ -301,11 +339,7 @@ const configuration_workflow = () =>
301
339
  type: "String",
302
340
  required: true,
303
341
  attributes: {
304
- options: [
305
- { value: "if-offered", label: "if-offered (Standard)" },
306
- { value: "required", label: "required (strikt)" },
307
- { value: "disabled", label: "disabled (aus)" },
308
- ],
342
+ options: ["if-offered", "required", "disabled"],
309
343
  },
310
344
  default: "if-offered",
311
345
  }),
@@ -321,11 +355,7 @@ const configuration_workflow = () =>
321
355
  type: "String",
322
356
  required: true,
323
357
  attributes: {
324
- options: [
325
- { value: "if-offered", label: "if-offered (Standard)" },
326
- { value: "required", label: "required (strikt)" },
327
- { value: "disabled", label: "disabled (aus)" },
328
- ],
358
+ options: ["if-offered", "required", "disabled"],
329
359
  },
330
360
  default: "if-offered",
331
361
  }),
@@ -801,21 +831,99 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
801
831
  // (TCP + Negotiate + Session + TREE_CONNECT + Auth) works, which
802
832
  // is all the connection test actually promises.
803
833
  const rel = testCfg.base_path ? sanitizeRelativePath(testCfg.base_path) : "";
834
+ // When a base_path is set, verify it exists and is a directory
835
+ // BEFORE trying to enumerate it. This turns the opaque
836
+ // "CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND" into a clear
837
+ // "Basispfad existiert nicht" hint the user can act on.
838
+ if (rel) {
839
+ let st;
840
+ try {
841
+ st = await client.stat(rel);
842
+ } catch (statErr) {
843
+ const smsg = String((statErr && statErr.message) || statErr || "");
844
+ if (/OBJECT_NAME_NOT_FOUND|OBJECT_PATH_NOT_FOUND|ENOENT|STATUS_NO_SUCH_FILE/i.test(smsg)) {
845
+ // Try to list the parent directory so we can *show* the
846
+ // user what the server actually reports at that level.
847
+ // This helps distinguish typo vs. case-mismatch vs.
848
+ // permission-hidden entry. If the parent is the share
849
+ // root we can't list it (known smb3-client / Samba bug),
850
+ // so we just skip the sibling probe in that case.
851
+ const parts = rel.split("/").filter(Boolean);
852
+ const missing = parts[parts.length - 1];
853
+ const parent = parts.slice(0, -1).join("/");
854
+ let siblings = null;
855
+ if (parent) {
856
+ try {
857
+ const listing = await client.readdir(parent);
858
+ siblings = Array.isArray(listing)
859
+ ? listing.map((d) => ({
860
+ name: d && (d.name || d),
861
+ isDirectory: !!(d && (d.isDirectory === true || (typeof d.isDirectory === "function" && d.isDirectory()))),
862
+ }))
863
+ : null;
864
+ } catch (_) {
865
+ siblings = null;
866
+ }
867
+ }
868
+ const hint =
869
+ "Der Basispfad \u201e" + rel + "\u201c existiert auf der " +
870
+ "Freigabe \u201e" + testCfg.share + "\u201c nicht " +
871
+ "(oder ist f\u00fcr den angemeldeten Benutzer nicht " +
872
+ "sichtbar). Bitte Schreibweise, Gro\u00df-/Kleinschreibung " +
873
+ "und Zugriffsrechte pr\u00fcfen. Der Basispfad ist relativ " +
874
+ "zur Freigabe \u2014 also z.\u202fB. \u201eprojekte/2026\u201c, " +
875
+ "nicht \u201e/mnt/\u2026\u201c.";
876
+ const e = new Error(hint);
877
+ e.cause = statErr;
878
+ e.code = "BASE_PATH_NOT_FOUND";
879
+ e.diagnostics = {
880
+ missing_segment: missing,
881
+ parent_path: parent || "(Share-Root)",
882
+ parent_listable: siblings !== null,
883
+ siblings: siblings,
884
+ };
885
+ throw e;
886
+ }
887
+ throw statErr;
888
+ }
889
+ if (st && st.isDirectory === false && st.isFile === true) {
890
+ const e = new Error(
891
+ "Der Basispfad \u201e" + rel + "\u201c ist eine Datei, kein " +
892
+ "Verzeichnis. Bitte tragen Sie einen Ordnernamen ein."
893
+ );
894
+ e.code = "BASE_PATH_NOT_A_DIR";
895
+ throw e;
896
+ }
897
+ }
804
898
  try {
805
899
  return await client.readdir(rel);
806
900
  } catch (err) {
807
- const msg = String((err && err.message) || err || "");
901
+ // Look at both the wrapper error and its underlying cause.
902
+ const causeMsg = String((err && err.cause && err.cause.message) || "");
903
+ const msg = String((err && err.message) || err || "") + " " + causeMsg;
808
904
  const isRootProbe = !rel;
809
- const isNameInvalid = /0xC0000033|OBJECT_NAME_INVALID/i.test(msg);
810
- if (isRootProbe && isNameInvalid) {
811
- // Fall back: proof-of-life via stat on share root.
812
- await client.stat("");
813
- return [];
905
+ const isRootEnumBug = /0xC0000033|OBJECT_NAME_INVALID|Share-Root/i.test(msg);
906
+ if (isRootProbe && isRootEnumBug) {
907
+ // Fall back: proof-of-life via stat on share root. This
908
+ // confirms TCP + Negotiate + Session + Auth + TREE_CONNECT
909
+ // without hitting the broken QUERY_DIRECTORY path.
910
+ try {
911
+ await client.stat("");
912
+ // Signal to the caller that the connection works, but
913
+ // the share root cannot be enumerated on this server.
914
+ const marker = [];
915
+ marker._rootNotEnumerable = true;
916
+ return marker;
917
+ } catch (statErr) {
918
+ // stat also failed — surface the original error.
919
+ throw err;
920
+ }
814
921
  }
815
922
  throw err;
816
923
  }
817
924
  });
818
925
  const took = Date.now() - started;
926
+ const rootNotEnum = Array.isArray(listing) && listing._rootNotEnumerable === true;
819
927
  return res.json({
820
928
  ok: true,
821
929
  server: testCfg.server,
@@ -831,6 +939,13 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
831
939
  isDirectory: !!(e && (e.isDirectory === true || (e.stats && e.stats.isDirectory && e.stats.isDirectory()))),
832
940
  })),
833
941
  truncated: Array.isArray(listing) && listing.length > 20,
942
+ note: rootNotEnum
943
+ ? "Verbindung + Anmeldung erfolgreich. Der Server erlaubt jedoch " +
944
+ "kein direktes Auflisten des Share-Roots (bekanntes Samba-" +
945
+ "Verhalten mit smb3-client). Bitte setzen Sie einen Basispfad " +
946
+ "in der Plugin-Config (z.\u202fB. einen Unterordner der " +
947
+ "Freigabe) — dann funktioniert der File-Manager vollständig."
948
+ : undefined,
834
949
  });
835
950
  } catch (err) {
836
951
  // Turn opaque SMB / socket errors into actionable hints.
@@ -873,6 +988,7 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
873
988
  error: msg,
874
989
  code,
875
990
  hint,
991
+ diagnostics: (err && err.diagnostics) || undefined,
876
992
  attempted: {
877
993
  server: testCfg.server,
878
994
  share: testCfg.share,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Saltcorn plugin: browse, upload, rename and delete files on a Samba/CIFS share via SMB 3.1.1 (AES-CMAC signing, optional encryption). File-manager view, directory tree, inline PDF viewer, external-app open (smb://).",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/smb-client.js CHANGED
@@ -279,36 +279,54 @@ async function buildClient(config) {
279
279
  } catch (err) {
280
280
  // Some Samba builds (observed on 4.20+/4.23+) reject
281
281
  // SMB2 QUERY_DIRECTORY on the *share root* with
282
- // STATUS_OBJECT_NAME_INVALID (0xC0000033) because the CREATE
283
- // used an empty filename. The fallback below re-issues the
284
- // request with an explicit path (".") which most Samba VFS
285
- // modules accept for the current directory.
286
- const status = (err && (err.status || err.code)) || "";
287
- const msg = String((err && err.message) || err || "");
282
+ // STATUS_OBJECT_NAME_INVALID (0xC0000033) because smb3-client
283
+ // asks for FileIdBothDirectoryInformation (class 37) on a
284
+ // handle opened with an empty filename. We cannot swap the
285
+ // information class from userland, and Samba refuses both
286
+ // "." ( STATUS_OBJECT_NAME_NOT_FOUND) and "*" (→ CREATE with
287
+ // wildcard is protocol-illegal). The clean way out is:
288
+ // require the caller to configure a real base_path so every
289
+ // readdir happens inside a directory the server can enumerate.
290
+ const msg = String((err && err.message) || err || "");
288
291
  const isRoot = !rel && !basePath;
289
- const isNameInvalid =
290
- /0xC0000033|STATUS_OBJECT_NAME_INVALID|OBJECT_NAME_INVALID/i.test(msg) ||
291
- status === 0xc0000033;
292
- if (isRoot && isNameInvalid) {
293
- try {
294
- dirents = await client.readdir(shareName + "/.", { withFileTypes: true });
295
- } catch (_) {
296
- // Second fallback: an explicit wildcard segment.
297
- try {
298
- dirents = await client.readdir(shareName + "/*", { withFileTypes: true });
299
- } catch (_) {
300
- const e = new Error(
301
- "QUERY_DIRECTORY auf dem Share-Root schlug fehl (" +
302
- (msg || "OBJECT_NAME_INVALID") + "). " +
303
- "Setzen Sie einen Basispfad im Plugin-Konfig oder prüfen Sie die Share-Definition auf dem Server."
304
- );
305
- e.cause = err;
306
- throw e;
307
- }
308
- }
309
- } else {
310
- throw err;
292
+ const isRootEnumBug =
293
+ /0xC0000033|OBJECT_NAME_INVALID|QUERY_DIRECTORY/i.test(msg);
294
+ if (isRoot && isRootEnumBug) {
295
+ const e = new Error(
296
+ "Das Share-Root lässt sich auf diesem Samba-Server nicht " +
297
+ "direkt auflisten (" + (msg || "QUERY_DIRECTORY failed") + "). " +
298
+ "Bitte in der Plugin-Config einen Basispfad setzen (z.\u202fB. " +
299
+ "einen Unterordner der Freigabe wie „daten“ oder „projekte“) — " +
300
+ "dann funktionieren alle Directory-Listings innerhalb dieses " +
301
+ "Unterordners. Details siehe README, Abschnitt „Troubleshooting " +
302
+ "QUERY_DIRECTORY auf Share-Root“."
303
+ );
304
+ e.cause = err;
305
+ throw e;
311
306
  }
307
+ // A missing directory (either the base_path itself or a
308
+ // sub-directory the caller asked to list) surfaces as CREATE
309
+ // failing with STATUS_OBJECT_NAME_NOT_FOUND / OBJECT_PATH_NOT_FOUND
310
+ // (both reported by smb3-client with an ENOENT tail). The raw
311
+ // "CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND (ENOENT)" is not
312
+ // actionable for end users, so rewrap it into a German hint that
313
+ // names the actual missing path.
314
+ const isMissing =
315
+ /OBJECT_NAME_NOT_FOUND|OBJECT_PATH_NOT_FOUND|ENOENT|STATUS_NO_SUCH_FILE/i.test(msg);
316
+ if (isMissing) {
317
+ const shown = full.replace(/^[^/]+\/?/, "") || "(Share-Root)";
318
+ const e = new Error(
319
+ "Der Pfad „" + shown + "“ existiert auf der Freigabe „" +
320
+ shareName + "“ nicht (oder ist für den angemeldeten Benutzer " +
321
+ "nicht sichtbar). Bitte Schreibweise, Groß-/Kleinschreibung " +
322
+ "und Zugriffsrechte prüfen. Ursprüngliche Server-Antwort: " +
323
+ (msg || "CREATE failed")
324
+ );
325
+ e.cause = err;
326
+ e.code = "ENOENT";
327
+ throw e;
328
+ }
329
+ throw err;
312
330
  }
313
331
  // Parallel enrichment. Bounded to a reasonable concurrency to avoid
314
332
  // saturating the SMB session on huge directories.