Merge pull request #278 from meiiie/fix/export-media-paths

fix(export): reopen saved videos and default to source quality
This commit is contained in:
webadderall
2026-04-20 20:35:21 +10:00
committed by GitHub
6 changed files with 107 additions and 16 deletions
+76
View File
@@ -0,0 +1,76 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("local media path policy", () => {
let tempRoot: string;
let appDataPath: string;
let userDataPath: string;
let tempPath: string;
let appPath: string;
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-media-policy-"));
appDataPath = path.join(tempRoot, "AppData");
userDataPath = path.join(tempRoot, "UserData");
tempPath = path.join(tempRoot, "Temp");
appPath = path.join(tempRoot, "App");
await Promise.all(
[appDataPath, userDataPath, tempPath, appPath].map((dirPath) =>
fs.mkdir(dirPath, { recursive: true }),
),
);
vi.resetModules();
vi.doMock("electron", () => ({
app: {
isPackaged: false,
getAppPath: () => appPath,
getPath: (name: string) => {
if (name === "appData") return appDataPath;
if (name === "userData") return userDataPath;
if (name === "temp") return tempPath;
return tempRoot;
},
setPath: () => undefined,
},
}));
});
afterEach(async () => {
vi.resetModules();
vi.doUnmock("electron");
if (tempRoot) {
await fs.rm(tempRoot, { recursive: true, force: true });
}
});
it("allows existing exported media files outside the session directories", async () => {
const downloadsPath = path.join(tempRoot, "Downloads");
const exportPath = path.join(downloadsPath, "export-test.mp4");
await fs.mkdir(downloadsPath, { recursive: true });
await fs.writeFile(exportPath, "test-video");
const { isAllowedLocalMediaPath } = await import("./manager");
await expect(isAllowedLocalMediaPath(exportPath)).resolves.toBe(true);
});
it("rejects missing media files outside the allowed directories", async () => {
const missingPath = path.join(tempRoot, "Downloads", "missing.mp4");
const { isAllowedLocalMediaPath } = await import("./manager");
await expect(isAllowedLocalMediaPath(missingPath)).resolves.toBe(false);
});
it("allows approved media paths before the file exists", async () => {
const pendingExportPath = path.join(tempRoot, "Downloads", "pending-export.mp4");
const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import("./manager");
await rememberApprovedLocalReadPath(pendingExportPath);
await expect(isAllowedLocalMediaPath(pendingExportPath)).resolves.toBe(true);
});
});
+7
View File
@@ -60,6 +60,13 @@ export function isAllowedLocalReadPath(candidatePath: string) {
);
}
// Keep media-server access rules aligned with read-local-file so exported videos
// saved outside the active recording session can still be reopened in the editor.
export async function isAllowedLocalMediaPath(candidatePath: string) {
const normalizedCandidatePath = normalizePath(candidatePath);
return isAllowedLocalReadPath(normalizedCandidatePath);
}
export async function rememberApprovedLocalReadPath(filePath?: string | null) {
const normalizedPath = normalizeVideoSourcePath(filePath);
if (!normalizedPath) {
+4 -1
View File
@@ -29,6 +29,7 @@ import {
muxExportedVideoAudioBuffer,
type NativeVideoExportSession,
} from "../export/native-video";
import { approveUserPath } from "../utils";
export function registerExportHandlers() {
ipcMain.handle(
@@ -341,6 +342,7 @@ export function registerExportHandlers() {
}
await fs.writeFile(result.filePath, Buffer.from(videoData));
approveUserPath(result.filePath);
return {
success: true,
@@ -362,10 +364,11 @@ export function registerExportHandlers() {
const resolvedPath = path.resolve(outputPath)
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
await fs.writeFile(resolvedPath, Buffer.from(videoData));
approveUserPath(resolvedPath);
return {
success: true,
path: outputPath,
path: resolvedPath,
message: 'Video exported successfully',
canceled: false,
};
+15 -14
View File
@@ -15,25 +15,26 @@ import {
setCurrentVideoPath,
currentRecordingSession,
setCurrentRecordingSession,
approvedLocalReadPaths,
} from "../state";
import { normalizeVideoSourcePath } from "../utils";
import { isPathInsideDirectory, replaceApprovedSessionLocalReadPaths } from "../project/manager";
import {
getProjectsDir,
isAllowedLocalMediaPath,
isPathInsideDirectory,
isTrustedProjectPath,
listProjectLibraryEntries,
loadProjectFromPath,
persistRecordingsDirectorySetting,
replaceApprovedSessionLocalReadPaths,
rememberRecentProject,
saveProjectThumbnail,
} from "../project/manager";
import {
getTelemetryPathForVideo,
isAutoRecordingPath,
getRecordingsDir,
approveUserPath,
normalizeVideoSourcePath,
} from "../utils";
import {
getProjectsDir,
persistRecordingsDirectorySetting,
saveProjectThumbnail,
rememberRecentProject,
listProjectLibraryEntries,
loadProjectFromPath,
isTrustedProjectPath,
} from "../project/manager";
import { persistRecordingSessionManifest, resolveRecordingSession } from "../project/session";
function normalizeRecordingTimeOffsetMs(value: unknown): number {
@@ -391,8 +392,8 @@ export function registerProjectHandlers() {
} catch {
return { success: false as const };
}
if (!approvedLocalReadPaths.has(resolved) && !approvedLocalReadPaths.has(normalized)) {
console.warn(`[get-local-media-url] Blocked unapproved path: ${resolved}`);
if (!(await isAllowedLocalMediaPath(resolved))) {
console.warn(`[get-local-media-url] Blocked disallowed path: ${resolved}`);
return { success: false as const };
}
return { success: true as const, url: buildMediaUrl(baseUrl, resolved) };
@@ -52,6 +52,10 @@ describe("editorPreferences", () => {
).toEqual(DEFAULT_EDITOR_PREFERENCES);
});
it("defaults MP4 exports to source quality", () => {
expect(DEFAULT_EDITOR_PREFERENCES.exportQuality).toBe("source");
});
it("loads stored editor control preferences", () => {
vi.stubGlobal(
"localStorage",
@@ -838,7 +838,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
editor.exportQuality === "high" ||
editor.exportQuality === "source"
? editor.exportQuality
: "good",
: "source",
mp4FrameRate: normalizeExportMp4FrameRate(editor.mp4FrameRate),
exportFormat: editor.exportFormat === "gif" ? "gif" : "mp4",
gifFrameRate: