mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
Reference in New Issue
Block a user