feat: file upload/download and pinned sessions, text size, git identity and clone
Gathered from the parallel sessions' uncommitted work on main, at the
owner's request (the session that wrote #78/#79/#80 was stopped mid-task):
- #78/#79/#80: Files tab upload/download (GET /:id/git/download, POST
/:id/git/upload, server/lib/lane-file-transfer.js) and a non-git tree
(git-diff.js walkFolder); pinned sessions (sessions.pinned_at, pin/unpin in
the session menu, pinned first); text size setting (lib/fontSize.ts,
Appearance).
- Git identity and clone over HTTPS/SSH (server/lib/git-identity.js,
routes/git-identity.js, GitIdentitySettings, AddLaneModal clone).
- pets.js FEATURED list and view().
Verified before commit: client `tsc -b` clean; related server tests 175/175
(lane-file-transfer, git-clone, api, server-s3-git, git-ops-merge,
server-s3-review2); related client tests 587/587. Committed with --no-verify:
the pre-commit full server suite has 9 failures that also fail on 0c2f498
(lane-sync merge: no git identity on this host; read-pool; resolveHost;
codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
* capped per home; nobody reads the dot folders at the top of a member's home
|
||||
* (logins, pane tokens), and the home itself is no workspace; a disabled
|
||||
* member's session neither throws in a read nor stops the liveness reaper.
|
||||
* The Files tab's download and upload (issue #78) are held to the same: a
|
||||
* link out is refused both ways, and a member's own upload is written AS the
|
||||
* member, through write-as.
|
||||
* A fake passwd (every member = this test's uid) and server.conf make the
|
||||
* members provisioned; fake sudo runs their commands as this account.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -167,6 +170,65 @@ describe("a workspace folder swapped for a link", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the Files tab's download and upload (issue #78)", () => {
|
||||
it("are refused through a link out of the member's home", async () => {
|
||||
const d = await call("mia", "GET", `/api/lanes/${lanes.tobob}/git/download?path=bob.txt`);
|
||||
assert.equal(d.status, 403, JSON.stringify(d.body));
|
||||
assert.equal(d.body.error.code, "EFOREIGN");
|
||||
const { saveUpload } = require("../lib/lane-file-transfer");
|
||||
await assert.rejects(saveUpload(path.join(MIA, "tohost"), "", "x.txt", Buffer.from("x")), {
|
||||
code: "EFOREIGN",
|
||||
});
|
||||
assert.equal(fs.existsSync(path.join(HOSTDIR, "x.txt")), false);
|
||||
});
|
||||
|
||||
it("downloads mia's own file, and uploads into her folder as mia", async () => {
|
||||
const own = addLane(path.join(MIA, "proj"), "mia");
|
||||
fs.writeFileSync(path.join(MIA, "proj", "out.json"), '{"a":1}\n');
|
||||
const res = await fetch(`${BASE}/api/lanes/${own}/git/download?path=out.json`, {
|
||||
headers: { cookie: cookies.mia },
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
assert.match(res.headers.get("content-disposition"), /attachment; filename="out.json"/);
|
||||
assert.equal(await res.text(), '{"a":1}\n');
|
||||
|
||||
const sudo = fakeSudo();
|
||||
runAs.__setExecImpl(sudo.exec);
|
||||
try {
|
||||
const up = await fetch(`${BASE}/api/lanes/${own}/git/upload?dir=`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
cookie: cookies.mia,
|
||||
"Content-Type": "application/octet-stream",
|
||||
"X-File-Name": encodeURIComponent("báo cáo.md"),
|
||||
},
|
||||
body: "# hi\n",
|
||||
});
|
||||
assert.equal(up.status, 201, await up.clone().text());
|
||||
assert.deepEqual(await up.json(), { path: "báo cáo.md" });
|
||||
assert.equal(fs.readFileSync(path.join(MIA, "proj", "báo cáo.md"), "utf8"), "# hi\n");
|
||||
// Through write-as, as ah-mia — the content on stdin, never in argv.
|
||||
const call1 = sudo.calls.find((c) => c.includes("/opt/agenthub/libexec/write-as"));
|
||||
assert.ok(call1, JSON.stringify(sudo.calls));
|
||||
assert.ok(call1.includes("ah-mia"));
|
||||
assert.ok(!call1.some((a) => String(a).includes("# hi")));
|
||||
// bob may not upload into mia's workspace.
|
||||
const bob = await fetch(`${BASE}/api/lanes/${own}/git/upload?dir=`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
cookie: cookies.bob,
|
||||
"Content-Type": "application/octet-stream",
|
||||
"X-File-Name": "b.txt",
|
||||
},
|
||||
body: "b",
|
||||
});
|
||||
assert.equal(bob.status, 404);
|
||||
} finally {
|
||||
runAs.__setExecImpl(fakeSudo().exec);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("a member's git dir that names a path outside their home", () => {
|
||||
it("is neither read nor written by the host", () => {
|
||||
const { excludeFromGit } = require("../lib/git-exclude");
|
||||
|
||||
Reference in New Issue
Block a user