saltcorn-samba 0.4.2 → 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.
- package/CHANGELOG.md +77 -0
- package/index.js +163 -0
- package/package.json +1 -1
- package/smb-client.js +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,83 @@ 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
|
+
|
|
48
|
+
## [0.4.3] – 2026-07-05
|
|
49
|
+
|
|
50
|
+
### Fixed – **Unklare Meldung bei nicht-existierendem Basispfad**
|
|
51
|
+
|
|
52
|
+
Wenn ein Basispfad angegeben wurde, der auf dem Server nicht existiert
|
|
53
|
+
(oder für den angemeldeten Benutzer nicht sichtbar ist), meldete die
|
|
54
|
+
Test-Route bisher nur die rohe Server-Antwort:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND (ENOENT)
|
|
58
|
+
CREATE failed: STATUS_OBJECT_PATH_NOT_FOUND (ENOENT)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Daraus konnte der Benutzer nicht erkennen, ob es sich um einen Tippfehler,
|
|
62
|
+
um einen Klein-/Großschreibungs-Konflikt (Samba mit `case sensitive = yes`)
|
|
63
|
+
oder um eine Berechtigung handelt.
|
|
64
|
+
|
|
65
|
+
**Neu:**
|
|
66
|
+
|
|
67
|
+
1. Die Test-Route prüft den Basispfad jetzt zuerst mit `stat()`, bevor sie
|
|
68
|
+
`readdir()` versucht. Fehlt der Ordner, wird eine deutsche Meldung
|
|
69
|
+
zurückgegeben, die den betroffenen Pfad, die Freigabe und typische
|
|
70
|
+
Ursachen (Schreibweise, Groß-/Kleinschreibung, Zugriffsrechte) nennt.
|
|
71
|
+
2. Zusätzlich liefert die Route ein `diagnostics`-Objekt zurück, das
|
|
72
|
+
den fehlenden Segmentnamen, den übergeordneten Pfad und — sofern der
|
|
73
|
+
übergeordnete Ordner auflistbar ist — dessen tatsächliche Einträge
|
|
74
|
+
enthält. Das Test-UI hebt ähnlich geschriebene Nachbareinträge hervor,
|
|
75
|
+
damit Tippfehler oder Case-Mismatch sofort sichtbar werden.
|
|
76
|
+
3. Der `smb-client.js`-`readdir()`-Wrapper mappt `OBJECT_NAME_NOT_FOUND`
|
|
77
|
+
und `OBJECT_PATH_NOT_FOUND` ebenfalls auf eine deutsche Meldung, damit
|
|
78
|
+
auch der File-Manager (außerhalb der Test-Route) verständlich über
|
|
79
|
+
fehlende Ordner informiert.
|
|
80
|
+
|
|
81
|
+
**Kein Config-Migrationsschritt nötig.** Wer die neuen Diagnose-Boxen
|
|
82
|
+
sehen will, muss lediglich `pv-host/saltcorn-samba@0.4.3` einspielen.
|
|
83
|
+
|
|
7
84
|
## [0.4.2] – 2026-07-05
|
|
8
85
|
|
|
9
86
|
### Fixed – **Dropdown-Felder zeigen `[object Object]` statt Optionen**
|
package/index.js
CHANGED
|
@@ -184,6 +184,63 @@ window.sambaTestConn = async function(btn) {
|
|
|
184
184
|
'Fehler: <code>' + String(data && data.error || 'Unbekannt').replace(/[<>&]/g,'?') + '</code>' +
|
|
185
185
|
(data && data.code ? ' <span class="text-muted">(' + data.code + ')</span>' : '') + '<br>' +
|
|
186
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 {'<':'<','>':'>','&':'&'}[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
|
+
// 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
|
+
}
|
|
207
|
+
if (!d.parent_listable) {
|
|
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>';
|
|
216
|
+
} else if (sibs.length === 0) {
|
|
217
|
+
box += '<br>Der übergeordnete Ordner ist leer.';
|
|
218
|
+
} else {
|
|
219
|
+
if (similar.length) {
|
|
220
|
+
box += '<br><b>Ähnliche Einträge, die tatsächlich existieren:</b><ul style="margin:.3rem 0 .3rem 1rem">';
|
|
221
|
+
similar.slice(0, 10).forEach(function(s){ box += '<li>' + (s.isDirectory?'📁 ':'📄 ') + '<code>' + esc(s.name) + '</code></li>'; });
|
|
222
|
+
box += '</ul>';
|
|
223
|
+
}
|
|
224
|
+
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">';
|
|
225
|
+
sibs.slice(0, 100).forEach(function(s){ box += '<li>' + (s.isDirectory?'📁 ':'📄 ') + '<code>' + esc(s.name) + '</code></li>'; });
|
|
226
|
+
if (sibs.length > 100) box += '<li><i>… (' + (sibs.length - 100) + ' weitere)</i></li>';
|
|
227
|
+
box += '</ul></details>';
|
|
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
|
+
}
|
|
241
|
+
box += '</div>';
|
|
242
|
+
return box;
|
|
243
|
+
})()) +
|
|
187
244
|
(a.server ? (
|
|
188
245
|
'<details style="margin-top:.4rem"><summary>Versuchte Verbindungsdaten</summary>' +
|
|
189
246
|
'<table class="table table-sm" style="margin-top:.4rem">' +
|
|
@@ -797,6 +854,17 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
797
854
|
// (TCP + Negotiate + Session + TREE_CONNECT + Auth) works, which
|
|
798
855
|
// is all the connection test actually promises.
|
|
799
856
|
const rel = testCfg.base_path ? sanitizeRelativePath(testCfg.base_path) : "";
|
|
857
|
+
// When a base_path is set, verify it exists and is a directory
|
|
858
|
+
// BEFORE trying to enumerate it. This turns the opaque
|
|
859
|
+
// "CREATE failed: STATUS_OBJECT_NAME_NOT_FOUND" into a clear
|
|
860
|
+
// "Basispfad existiert nicht" hint the user can act on.
|
|
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.
|
|
800
868
|
try {
|
|
801
869
|
return await client.readdir(rel);
|
|
802
870
|
} catch (err) {
|
|
@@ -821,6 +889,100 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
821
889
|
throw err;
|
|
822
890
|
}
|
|
823
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
|
+
}
|
|
824
986
|
throw err;
|
|
825
987
|
}
|
|
826
988
|
});
|
|
@@ -890,6 +1052,7 @@ code{background:#f4f4f4;padding:2px 6px;border-radius:3px;word-break:break-all}<
|
|
|
890
1052
|
error: msg,
|
|
891
1053
|
code,
|
|
892
1054
|
hint,
|
|
1055
|
+
diagnostics: (err && err.diagnostics) || undefined,
|
|
893
1056
|
attempted: {
|
|
894
1057
|
server: testCfg.server,
|
|
895
1058
|
share: testCfg.share,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saltcorn-samba",
|
|
3
|
-
"version": "0.4.
|
|
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": {
|
package/smb-client.js
CHANGED
|
@@ -304,6 +304,28 @@ async function buildClient(config) {
|
|
|
304
304
|
e.cause = err;
|
|
305
305
|
throw e;
|
|
306
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
|
+
}
|
|
307
329
|
throw err;
|
|
308
330
|
}
|
|
309
331
|
// Parallel enrichment. Bounded to a reasonable concurrency to avoid
|