diff --git a/src/control/commands/capture-session.ts b/src/control/commands/capture-session.ts index ce6c72ac8e..6f4d686186 100644 --- a/src/control/commands/capture-session.ts +++ b/src/control/commands/capture-session.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import { chmod, mkdir } from "node:fs/promises"; import { withInterrupt } from "@genesiscz/utils/cli/interrupt"; import { SafeJSON } from "@genesiscz/utils/json"; import { logger, out } from "@genesiscz/utils/logger"; @@ -180,6 +181,9 @@ export function registerCaptureSessionCommands(capture: Command): void { throw new Error("Nothing on screen to record."); } + // The default output and every session folder live here, so the folder is owner only. + await mkdir(recordingsDir(), { recursive: true, mode: 0o700 }); + await chmod(recordingsDir(), 0o700); session = new RecordingSession({ target, directory: recordingsDir(`session-${randomUUID()}`), diff --git a/src/control/lib/recording-session.test.ts b/src/control/lib/recording-session.test.ts index 62784802f3..640085c9fd 100644 --- a/src/control/lib/recording-session.test.ts +++ b/src/control/lib/recording-session.test.ts @@ -288,6 +288,33 @@ describe("RecordingSession", () => { expect(await recording.finished).toBe("discarded"); }); + it("keeps its segment folder private, also when the folder already exists with wider permissions", async () => { + for (const existing of [false, true]) { + const directory = join(root, existing ? "existing" : "new"); + if (existing) { + await mkdir(directory, { mode: 0o755 }); + } + + const { recorder } = fakeRecorder(); + const recording = new RecordingSession({ + target: finder, + directory, + output: join(root, "private.mp4"), + recorder, + join: async () => ({ durationMs: 0 }), + emit: () => {}, + now: () => clock, + tickMs: 0, + slowStartMs: 0, + }); + await recording.start(); + + expect((await fs.stat(directory)).mode & 0o777).toBe(0o700); + await recording.command({ cmd: "discard" }); + expect(await recording.finished).toBe("discarded"); + } + }); + it("a failed join keeps a movie already at the output and leaves no temporary file", async () => { const { recorder } = fakeRecorder(); await writeFile(join(root, "out.mp4"), "previous movie"); diff --git a/src/control/lib/recording-session.ts b/src/control/lib/recording-session.ts index 7286d14c38..ea43a7b606 100644 --- a/src/control/lib/recording-session.ts +++ b/src/control/lib/recording-session.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; -import { copyFile, mkdir, rename, rm, unlink } from "node:fs/promises"; +import { chmod, copyFile, mkdir, rename, rm, unlink } from "node:fs/promises"; import { dirname, join } from "node:path"; import { SafeJSON } from "@genesiscz/utils/json"; import { logger } from "@genesiscz/utils/logger"; @@ -198,7 +198,9 @@ export class RecordingSession { start(): Promise { return this.enqueue(async () => { try { - await mkdir(this.options.directory, { recursive: true }); + // Screen recordings are private: owner only. `mode` covers a new folder, chmod an existing one. + await mkdir(this.options.directory, { recursive: true, mode: 0o700 }); + await chmod(this.options.directory, 0o700); } catch (error) { log.warn({ error }, "could not create recording directory"); this.end("failed");