saltcorn-samba 0.4.3 → 0.4.4

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/index.js +125 -61
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,47 @@ 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.4] – 2026-07-05
8
+
9
+ ### Fixed – **Falscher `NAME_NOT_FOUND` durch `stat()`-Vorprüfung**
10
+
11
+ Die 0.4.3-Vorprüfung mit `client.stat(base_path)` schlug bei einigen
12
+ Samba-Servern fehl, obwohl der Ordner existiert und per `readdir()`
13
+ zugänglich ist. Ursache: `smb3-client` schickt in `stat()` ein CREATE
14
+ ohne `DIRECTORY_FILE`-Flag (`createOptions: 0`); manche Samba-
15
+ Konfigurationen (v.a. mit „access based enumeration“ oder speziellen
16
+ POSIX-ACLs) beantworten das mit `NAME_NOT_FOUND`, während derselbe
17
+ Ordner mit `readdir()` (`createOptions: 1 = DIRECTORY_FILE`) einwandfrei
18
+ geht.
19
+
20
+ **Fix:** Die Test-Route ruft direkt `readdir(base_path)` auf und fängt
21
+ den Fehler ab. Das ist auch semantisch korrekter – wir wollen wissen,
22
+ ob der Basispfad *aufgelistet* werden kann, nicht nur, ob er sich
23
+ öffnen lässt.
24
+
25
+ ### Added – **Case-/Schreibvarianten-Test bei fehlender Basispfad-Erkennung**
26
+
27
+ Schlägt der `readdir(base_path)`-Aufruf mit `NAME_NOT_FOUND` /
28
+ `PATH_NOT_FOUND` fehl, probiert die Test-Route zusätzlich:
29
+
30
+ - den Namen in UPPERCASE,
31
+ - den Namen in lowercase,
32
+ - den Namen als Capitalised.
33
+
34
+ Gelingt eine dieser Varianten, wird der tatsächliche Name grün
35
+ hervorgehoben („Gefunden: Der Ordner existiert unter dem Namen …“) und
36
+ der User bekommt einen direkt umsetzbaren Fix. Schlagen alle Varianten
37
+ mit demselben Fehler fehl, liegt es fast sicher an Zugriffsrechten
38
+ (`hide unreadable = yes`) oder an `veto files` — die Meldung erklärt
39
+ beide Fälle.
40
+
41
+ Die Diagnose-Box im UI zeigt jetzt zusätzlich:
42
+
43
+ - eine grüne Markierung mit dem richtigen Ordnernamen (falls gefunden),
44
+ - eine ausklappbare Tabelle mit allen Schreibvarianten-Ergebnissen,
45
+ - die Fehlermeldung beim Auflisten des übergeordneten Ordners (falls
46
+ das ebenfalls scheitert).
47
+
7
48
  ## [0.4.3] – 2026-07-05
8
49
 
9
50
  ### Fixed – **Unklare Meldung bei nicht-existierendem Basispfad**
package/index.js CHANGED
@@ -200,8 +200,19 @@ window.sambaTestConn = async function(btn) {
200
200
  });
201
201
  var box = '<div style="margin-top:.4rem;padding:.4rem .6rem;background:#f8d7da;border:1px solid #f5c2c7;border-radius:.25rem">';
202
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
+ // Working alternative spelling has the highest signal.
204
+ if (d.working_alternative) {
205
+ box += '<div style="margin-top:.3rem;padding:.3rem .5rem;background:#d1e7dd;border:1px solid #a3cfbb;border-radius:.25rem;color:#0f5132"><b>\u2192 Gefunden:</b> Der Ordner existiert unter dem Namen \u201e<code>' + esc(d.working_alternative) + '</code>\u201c. Bitte diesen exakt so als Basispfad eintragen.</div>';
206
+ }
203
207
  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>';
208
+ box += '<br><span class="text-muted">(Der übergeordnete Ordner konnte nicht aufgelistet werden';
209
+ if (d.parent_path === '(Share-Root)') {
210
+ box += ' \u2014 die direkte Auflistung des Share-Roots ist mit smb3-client auf Samba aktuell blockiert';
211
+ }
212
+ if (d.parent_error) {
213
+ box += '. Server-Antwort: <code>' + esc(d.parent_error) + '</code>';
214
+ }
215
+ box += '.)</span>';
205
216
  } else if (sibs.length === 0) {
206
217
  box += '<br>Der übergeordnete Ordner ist leer.';
207
218
  } else {
@@ -215,6 +226,18 @@ window.sambaTestConn = async function(btn) {
215
226
  if (sibs.length > 100) box += '<li><i>… (' + (sibs.length - 100) + ' weitere)</i></li>';
216
227
  box += '</ul></details>';
217
228
  }
229
+ // Spelling / case probes — always show, even without a
230
+ // working alternative, because seeing all four probes fail
231
+ // with the same error strongly suggests a permission issue
232
+ // rather than a spelling issue.
233
+ if (Array.isArray(d.spelling_probes) && d.spelling_probes.length) {
234
+ box += '<details style="margin-top:.3rem"><summary>Schreibvarianten-Test</summary><table class="table table-sm" style="margin-top:.3rem;font-size:.85em">';
235
+ box += '<thead><tr><th>Variante</th><th>Ergebnis</th></tr></thead><tbody>';
236
+ d.spelling_probes.forEach(function(p){
237
+ box += '<tr><td><code>' + esc(p.candidate) + '</code></td><td>' + (p.ok ? '✓ auflistbar' : '✗ <span class="text-muted">' + esc(String(p.error||'').split(/[\r\n]/)[0].slice(0,140)) + '</span>') + '</td></tr>';
238
+ });
239
+ box += '</tbody></table></details>';
240
+ }
218
241
  box += '</div>';
219
242
  return box;
220
243
  })()) +
@@ -835,66 +858,13 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
835
858
  // BEFORE trying to enumerate it. This turns the opaque
836
859
  // "CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND" into a clear
837
860
  // "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
- }
861
+ // We used to run a stat() first. That turned out to be flaky:
862
+ // smb3-client's stat() sends a CREATE with createOptions=0
863
+ // (no directory hint) which some Samba configurations reject
864
+ // for directories with strict ACLs. readdir() sends
865
+ // createOptions=1 (DIRECTORY_FILE) and is the right primitive
866
+ // for a base_path check anyway we want to know the folder
867
+ // can actually be listed, not just opened.
898
868
  try {
899
869
  return await client.readdir(rel);
900
870
  } catch (err) {
@@ -919,6 +889,100 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
919
889
  throw err;
920
890
  }
921
891
  }
892
+ // A non-existent base_path (or one hidden from this user by
893
+ // Samba's "hide unreadable" behaviour) surfaces here. Try to
894
+ // gather actionable diagnostics: list the parent directory
895
+ // (if not the share root) and probe common alternative
896
+ // spellings of the missing segment so we can distinguish
897
+ // typo / case-mismatch / permission problems.
898
+ const isMissing = /OBJECT_NAME_NOT_FOUND|OBJECT_PATH_NOT_FOUND|ENOENT|STATUS_NO_SUCH_FILE|existiert.*nicht/i.test(msg);
899
+ if (rel && isMissing) {
900
+ const parts = rel.split("/").filter(Boolean);
901
+ const missing = parts[parts.length - 1];
902
+ const parent = parts.slice(0, -1).join("/");
903
+ const parentAbs = parent || "(Share-Root)";
904
+ let siblings = null;
905
+ let parent_error = null;
906
+ if (parent) {
907
+ try {
908
+ const listing = await client.readdir(parent);
909
+ siblings = Array.isArray(listing)
910
+ ? listing.map((d) => ({
911
+ name: d && (d.name || d),
912
+ isDirectory: !!(d && (d.isDirectory === true || (typeof d.isDirectory === "function" && d.isDirectory()))),
913
+ }))
914
+ : null;
915
+ } catch (parentErr) {
916
+ parent_error = String((parentErr && parentErr.message) || parentErr || "");
917
+ }
918
+ }
919
+ // Probe alternate spellings: original, upper, lower, capitalised.
920
+ // This lets us report "Ordner existiert unter anderem Namen" if
921
+ // Samba is running with case sensitive = yes / case-preserved.
922
+ const probes = [];
923
+ const seen = new Set();
924
+ const addProbe = (name) => {
925
+ if (!name || seen.has(name)) return;
926
+ seen.add(name);
927
+ probes.push(name);
928
+ };
929
+ addProbe(missing);
930
+ addProbe(missing.toUpperCase());
931
+ addProbe(missing.toLowerCase());
932
+ addProbe(missing.charAt(0).toUpperCase() + missing.slice(1).toLowerCase());
933
+ const probe_results = [];
934
+ for (const p of probes) {
935
+ const candidate = parent ? parent + "/" + p : p;
936
+ let ok = false;
937
+ let perr = null;
938
+ try {
939
+ await client.readdir(candidate);
940
+ ok = true;
941
+ } catch (pErr) {
942
+ perr = String((pErr && pErr.message) || pErr || "");
943
+ }
944
+ probe_results.push({ candidate: p, ok, error: ok ? null : perr });
945
+ }
946
+ const workingAlt = probe_results.find(
947
+ (r) => r.ok && r.candidate !== missing
948
+ );
949
+ let hintText;
950
+ if (workingAlt) {
951
+ hintText =
952
+ "Der Ordner heisst auf dem Server \u201e" +
953
+ workingAlt.candidate +
954
+ "\u201c (andere Gross-/Kleinschreibung). Bitte den " +
955
+ "Basispfad exakt so eintragen \u2014 der Samba-Server ist " +
956
+ "case-sensitive (\u201ecase sensitive = yes\u201c in smb.conf).";
957
+ } else {
958
+ hintText =
959
+ "Der Basispfad \u201e" + rel + "\u201c ist auf dem " +
960
+ "Server nicht auffindbar. M\u00f6gliche Ursachen: " +
961
+ "(a) der Ordner existiert wirklich nicht (bitte mit " +
962
+ "einem SMB-Client wie \u201esmbclient\u201c oder dem " +
963
+ "Windows-Explorer gegenpr\u00fcfen); " +
964
+ "(b) der angemeldete Benutzer \u201e" +
965
+ (testCfg.username || "(anonymous)") +
966
+ "\u201c hat kein Leserecht auf den Ordner (Samba antwortet " +
967
+ "dann bei \u201ehide unreadable = yes\u201c mit " +
968
+ "NAME_NOT_FOUND statt ACCESS_DENIED); " +
969
+ "(c) der Ordner ist per \u201eveto files\u201c / " +
970
+ "\u201ehide files\u201c auf dem Server ausgeblendet.";
971
+ }
972
+ const e = new Error(hintText);
973
+ e.cause = err;
974
+ e.code = "BASE_PATH_NOT_FOUND";
975
+ e.diagnostics = {
976
+ missing_segment: missing,
977
+ parent_path: parentAbs,
978
+ parent_listable: siblings !== null,
979
+ parent_error: parent_error,
980
+ siblings: siblings,
981
+ spelling_probes: probe_results,
982
+ working_alternative: workingAlt ? workingAlt.candidate : null,
983
+ };
984
+ throw e;
985
+ }
922
986
  throw err;
923
987
  }
924
988
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saltcorn-samba",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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": {