@zhangfengshun/dsh-remote-ssh 2.1.4 → 2.1.5
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/lib/client.js +47 -0
- package/lib/index.js +70 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1078,6 +1078,53 @@ window.__ModuleLoader__.load({
|
|
|
1078
1078
|
return function () { if (el.parentNode) el.parentNode.removeChild(el); };
|
|
1079
1079
|
});
|
|
1080
1080
|
|
|
1081
|
+
// better-sidebar 0.15+ 的文件上传 UI 把原始字节流 POST 到 /sidebar/upload。
|
|
1082
|
+
// 远程工作区会话的文件树显示的是本地镜像目录,better-sidebar 会把上传写进
|
|
1083
|
+
// 本地镜像而不会到达远程。这里包装 fetch:目标目录位于某个远程工作区镜像
|
|
1084
|
+
// 目录内时,把该上传请求重定向到 /remote-ssh/upload(走 SSH 上传到远端)。
|
|
1085
|
+
ctx.effect(function () {
|
|
1086
|
+
if (typeof window === "undefined" || typeof window.fetch !== "function") return;
|
|
1087
|
+
var nativeFetch = window.fetch.bind(window);
|
|
1088
|
+
var mirrors = [];
|
|
1089
|
+
function refreshMirrors() {
|
|
1090
|
+
api("listWorkspaces").then(function (r) {
|
|
1091
|
+
if (r && r.ok && Array.isArray(r.workspaces)) {
|
|
1092
|
+
mirrors = r.workspaces.map(function (w) {
|
|
1093
|
+
return String(w && w.mirrorPath || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1094
|
+
}).filter(Boolean);
|
|
1095
|
+
}
|
|
1096
|
+
}).catch(function () {});
|
|
1097
|
+
}
|
|
1098
|
+
refreshMirrors();
|
|
1099
|
+
var timer = setInterval(refreshMirrors, 30000);
|
|
1100
|
+
function patchedFetch(input, init) {
|
|
1101
|
+
var raw = typeof input === "string" ? input : (input && input.url);
|
|
1102
|
+
if (typeof raw === "string") {
|
|
1103
|
+
try {
|
|
1104
|
+
var u = new URL(raw, window.location.origin);
|
|
1105
|
+
if (u.pathname === "/sidebar/upload") {
|
|
1106
|
+
var dir = (u.searchParams.get("dir") || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1107
|
+
var cwd = (u.searchParams.get("cwd") || "").replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
|
|
1108
|
+
for (var i = 0; i < mirrors.length; i++) {
|
|
1109
|
+
var m = mirrors[i];
|
|
1110
|
+
var hit = (dir && (dir === m || dir.indexOf(m + "/") === 0)) || (cwd && (cwd === m || cwd.indexOf(m + "/") === 0));
|
|
1111
|
+
if (hit) {
|
|
1112
|
+
var target = new URL("/remote-ssh/upload" + u.search, window.location.origin);
|
|
1113
|
+
return nativeFetch(target.toString(), init);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
} catch (e) {}
|
|
1118
|
+
}
|
|
1119
|
+
return nativeFetch(input, init);
|
|
1120
|
+
}
|
|
1121
|
+
window.fetch = patchedFetch;
|
|
1122
|
+
return function () {
|
|
1123
|
+
clearInterval(timer);
|
|
1124
|
+
if (window.fetch === patchedFetch) window.fetch = nativeFetch;
|
|
1125
|
+
};
|
|
1126
|
+
}, "dsh-remote-ssh: redirect remote-workspace uploads");
|
|
1127
|
+
|
|
1081
1128
|
if (betterSidebar) {
|
|
1082
1129
|
// 远程文件编辑器页签(隐藏页签,仅由代码打开,每个远程文件单开一栏)。
|
|
1083
1130
|
// remssh:files 和 remssh:term 已移除:远程文件由内置「文件」页签直接 SSH 读写,
|
package/lib/index.js
CHANGED
|
@@ -1672,6 +1672,76 @@ function apply(ctx, config) {
|
|
|
1672
1672
|
}), "dsh-remote-ssh: intercept /sidebar/api/" + m);
|
|
1673
1673
|
});
|
|
1674
1674
|
|
|
1675
|
+
// ---- better-sidebar 0.15+ 文件上传:远程工作区目录直接 SSH 上传到远端 ----
|
|
1676
|
+
// better-sidebar 的上传 UI 把原始字节流 POST 到它自己的 /sidebar/upload 路由,
|
|
1677
|
+
// 由本插件客户端将远程镜像目录下的这类请求转发到 /remote-ssh/upload。
|
|
1678
|
+
const REMOTE_UPLOAD_LIMIT = 134217728; // 与 better-sidebar 的上传上限一致
|
|
1679
|
+
|
|
1680
|
+
/** dir/cwd 是否位于某个远程工作区的本地镜像目录之下(大小写不敏感)。 */
|
|
1681
|
+
function matchRemoteWorkspace(dir, cwd) {
|
|
1682
|
+
const norm = (p) => String(p || "").replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
1683
|
+
const nd = norm(dir);
|
|
1684
|
+
const nc = norm(cwd);
|
|
1685
|
+
if (!nd && !nc) return null;
|
|
1686
|
+
const workspaces = settingsFace.read().workspaces || [];
|
|
1687
|
+
for (const w of workspaces) {
|
|
1688
|
+
const m = norm(w.mirrorPath);
|
|
1689
|
+
if (!m) continue;
|
|
1690
|
+
if ((nd && (nd === m || nd.startsWith(m + "/"))) || (nc && (nc === m || nc.startsWith(m + "/")))) return w;
|
|
1691
|
+
}
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
async function handleRemoteUpload(req, res) {
|
|
1696
|
+
if (!isTrusted(req)) { writeJson(res, 403, { ok: false, error: { code: "forbidden", message: "forbidden" } }); return; }
|
|
1697
|
+
if (req.method !== "POST") { writeJson(res, 405, { ok: false, error: { code: "method-error", message: "method not allowed" } }); return; }
|
|
1698
|
+
try {
|
|
1699
|
+
const url = new URL(req.url || "/", "http://dsh.internal");
|
|
1700
|
+
const dir = url.searchParams.get("dir") || "";
|
|
1701
|
+
const relativePath = (url.searchParams.get("relativePath") || "").replace(/\\/g, "/");
|
|
1702
|
+
const cwd = url.searchParams.get("cwd") || "";
|
|
1703
|
+
if (!dir || relativePath.trim() === "") { writeJson(res, 400, { ok: false, error: { code: "bad-request", message: "dir and relativePath are required" } }); return; }
|
|
1704
|
+
const ws = matchRemoteWorkspace(dir, cwd);
|
|
1705
|
+
if (!ws) { writeJson(res, 400, { ok: false, error: { code: "fs-error", message: "not a remote workspace directory" } }); return; }
|
|
1706
|
+
const remoteInfo = readRemoteInfoSync(ws.mirrorPath);
|
|
1707
|
+
const profile = remoteInfo && getProfile(remoteInfo.profileId);
|
|
1708
|
+
if (!remoteInfo || !profile) { writeJson(res, 500, { ok: false, error: { code: "internal", message: "remote workspace profile not found" } }); return; }
|
|
1709
|
+
let rel = relativePath;
|
|
1710
|
+
if (rel.startsWith("/")) rel = rel.slice(1);
|
|
1711
|
+
if (rel.split("/").some((seg) => seg === "..")) { writeJson(res, 400, { ok: false, error: { code: "bad-request", message: "relativePath must stay below the upload directory" } }); return; }
|
|
1712
|
+
const remoteDir = localToRemote(dir, ws.mirrorPath, remoteInfo.remotePath);
|
|
1713
|
+
const target = remoteDir.replace(/\/+$/, "") + (rel ? "/" + rel : "");
|
|
1714
|
+
const parent = target.slice(0, Math.max(target.lastIndexOf("/"), 0));
|
|
1715
|
+
// 读取原始字节流(File/Blob 直传,二进制安全)
|
|
1716
|
+
const chunks = [];
|
|
1717
|
+
let size = 0;
|
|
1718
|
+
for await (const chunk of req) {
|
|
1719
|
+
const b = Buffer.from(chunk);
|
|
1720
|
+
size += b.length;
|
|
1721
|
+
if (size > REMOTE_UPLOAD_LIMIT) { writeJson(res, 413, { ok: false, error: { code: "too-large", message: "upload exceeds the remote upload limit (" + REMOTE_UPLOAD_LIMIT + " bytes)" } }); return; }
|
|
1722
|
+
chunks.push(b);
|
|
1723
|
+
}
|
|
1724
|
+
const payloadBase64 = Buffer.concat(chunks).toString("base64");
|
|
1725
|
+
// 远端建父目录
|
|
1726
|
+
if (parent && parent !== remoteDir.replace(/\/+$/, "")) {
|
|
1727
|
+
const mk = await runPooled(profile, "mkdir -p " + shellQuotePath(parent), undefined, 65536);
|
|
1728
|
+
if (!mk.ok) { writeJson(res, 500, { ok: false, error: { code: "fs-error", message: String(mk.error || mk.stdout || "mkdir failed").trim() } }); return; }
|
|
1729
|
+
}
|
|
1730
|
+
// 二进制安全落地:base64 -d 还原后写入目标文件
|
|
1731
|
+
const w = await runPooled(profile, "base64 -d > " + shellQuotePath(target), payloadBase64, REMOTE_UPLOAD_LIMIT);
|
|
1732
|
+
if (!w.ok) { writeJson(res, 500, { ok: false, error: { code: "fs-error", message: String(w.error || w.stdout || "remote write failed").trim() } }); return; }
|
|
1733
|
+
writeOk(res, { path: dir.replace(/\\/g, "/").replace(/\/+$/, "") + "/" + rel, size: size });
|
|
1734
|
+
} catch (e) {
|
|
1735
|
+
writeJson(res, 400, { ok: false, error: { code: "fs-error", message: String(e && e.message ? e.message : e) } });
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
ctx.effect(() => ctx.webServer.register({
|
|
1740
|
+
kind: "exact",
|
|
1741
|
+
path: "/remote-ssh/upload",
|
|
1742
|
+
handler: function (req, res) { return handleRemoteUpload(req, res); }
|
|
1743
|
+
}), "dsh-remote-ssh: /remote-ssh/upload route");
|
|
1744
|
+
|
|
1675
1745
|
// ---- 清理 ----
|
|
1676
1746
|
ctx.effect(() => () => {
|
|
1677
1747
|
clearInterval(idleTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|