mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
fix(projects): tighten import and document project files
This commit is contained in:
1 parent
262d961cfb
commit
6e7342d16c
4 files changed
+218
-86
No files matched your search
+101
-80
@@ -1,22 +1,22 @@
|
||||
// @see - https://www.electron.build/configuration/configuration
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
||||
"appId": "dev.recordly.app",
|
||||
"electronUpdaterCompatibility": ">=2.16",
|
||||
"asar": true,
|
||||
"asarUnpack": [
|
||||
// @see - https://www.electron.build/configuration/configuration
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
||||
"appId": "dev.recordly.app",
|
||||
"electronUpdaterCompatibility": ">=2.16",
|
||||
"asar": true,
|
||||
"asarUnpack": [
|
||||
"node_modules/ffmpeg-static/**",
|
||||
"node_modules/ffprobe-static/**",
|
||||
"node_modules/uiohook-napi/**",
|
||||
"electron/native/**"
|
||||
],
|
||||
"productName": "Recordly",
|
||||
"npmRebuild": true,
|
||||
"buildDependenciesFromSource": true,
|
||||
"compression": "normal",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"electron/native/**"
|
||||
],
|
||||
"productName": "Recordly",
|
||||
"npmRebuild": true,
|
||||
"buildDependenciesFromSource": true,
|
||||
"compression": "normal",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-electron",
|
||||
@@ -27,67 +27,88 @@
|
||||
"!electron/native/**/build/**",
|
||||
"!*.png",
|
||||
"!preview*.png",
|
||||
"!*.md",
|
||||
"!README.md",
|
||||
"!CONTRIBUTING.md",
|
||||
"!LICENSE"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "public/wallpapers",
|
||||
"to": "assets/wallpapers"
|
||||
}
|
||||
],
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "webadderall",
|
||||
"repo": "Recordly",
|
||||
"tagNamePrefix": "v",
|
||||
"publishAutoUpdate": true
|
||||
}
|
||||
],
|
||||
|
||||
"mac": {
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
"icon": "icons/icons/mac/icon.icns",
|
||||
"artifactName": "${productName}-${arch}.${ext}",
|
||||
"extendInfo": {
|
||||
"NSAudioCaptureUsageDescription": "Recordly needs audio capture permission to record system audio.",
|
||||
"NSCameraUsageDescription": "Recordly needs camera access to record webcam video.",
|
||||
"NSMicrophoneUsageDescription": "Recordly needs microphone access to record voice audio.",
|
||||
"NSCameraUseContinuityCameraDeviceType": true,
|
||||
"com.apple.security.device.audio-input": true
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
],
|
||||
"icon": "icons/icons/png",
|
||||
"artifactName": "${productName}-linux-x64.${ext}",
|
||||
"category": "AudioVideo"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
],
|
||||
"icon": "icons/icons/win/icon.ico"
|
||||
,
|
||||
"executableName": "Recordly",
|
||||
"artifactName": "${productName}-windows-${arch}.${ext}"
|
||||
}
|
||||
}
|
||||
|
||||
"!*.md",
|
||||
"!README.md",
|
||||
"!CONTRIBUTING.md",
|
||||
"!LICENSE"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "public/wallpapers",
|
||||
"to": "assets/wallpapers"
|
||||
}
|
||||
],
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "webadderall",
|
||||
"repo": "Recordly",
|
||||
"tagNamePrefix": "v",
|
||||
"publishAutoUpdate": true
|
||||
}
|
||||
],
|
||||
|
||||
"mac": {
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
|
||||
"target": [
|
||||
{
|
||||
"target": "dmg",
|
||||
"arch": ["x64", "arm64"]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
"icon": "icons/icons/mac/icon.icns",
|
||||
"artifactName": "${productName}-${arch}.${ext}",
|
||||
"extendInfo": {
|
||||
"NSAudioCaptureUsageDescription": "Recordly needs audio capture permission to record system audio.",
|
||||
"NSCameraUsageDescription": "Recordly needs camera access to record webcam video.",
|
||||
"NSMicrophoneUsageDescription": "Recordly needs microphone access to record voice audio.",
|
||||
"NSCameraUseContinuityCameraDeviceType": true,
|
||||
"com.apple.security.device.audio-input": true,
|
||||
"CFBundleDocumentTypes": [
|
||||
{
|
||||
"CFBundleTypeName": "Recordly Project",
|
||||
"CFBundleTypeRole": "Editor",
|
||||
"CFBundleTypeExtensions": ["recordly"],
|
||||
"CFBundleTypeIconFile": "icon.icns",
|
||||
"LSHandlerRank": "Owner",
|
||||
"LSItemContentTypes": ["dev.recordly.app.project"]
|
||||
}
|
||||
],
|
||||
"UTExportedTypeDeclarations": [
|
||||
{
|
||||
"UTTypeIdentifier": "dev.recordly.app.project",
|
||||
"UTTypeDescription": "Recordly Project",
|
||||
"UTTypeConformsTo": ["public.json"],
|
||||
"UTTypeTagSpecification": {
|
||||
"public.filename-extension": ["recordly"],
|
||||
"public.mime-type": "application/x-recordly-project"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
],
|
||||
"icon": "icons/icons/png",
|
||||
"artifactName": "${productName}-linux-x64.${ext}",
|
||||
"category": "AudioVideo"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
],
|
||||
"icon": "icons/icons/win/icon.ico"
|
||||
,
|
||||
"executableName": "Recordly",
|
||||
"artifactName": "${productName}-windows-${arch}.${ext}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,9 @@ describe("local media path policy", () => {
|
||||
await fs.mkdir(downloadsPath, { recursive: true });
|
||||
await fs.writeFile(exportPath, "test-video");
|
||||
|
||||
const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import("./manager");
|
||||
const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import(
|
||||
"./manager"
|
||||
);
|
||||
|
||||
await expect(isAllowedLocalMediaPath(exportPath)).resolves.toBe(false);
|
||||
|
||||
@@ -71,7 +73,9 @@ describe("local media path policy", () => {
|
||||
|
||||
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");
|
||||
const { isAllowedLocalMediaPath, rememberApprovedLocalReadPath } = await import(
|
||||
"./manager"
|
||||
);
|
||||
|
||||
await rememberApprovedLocalReadPath(pendingExportPath);
|
||||
|
||||
@@ -131,7 +135,9 @@ describe("local media path policy", () => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { isAllowedLocalMediaPath, resolveApprovedLocalMediaPath } = await import("./manager");
|
||||
const { isAllowedLocalMediaPath, resolveApprovedLocalMediaPath } = await import(
|
||||
"./manager"
|
||||
);
|
||||
|
||||
await expect(isAllowedLocalMediaPath(symlinkInsideUserData)).resolves.toBe(false);
|
||||
await expect(resolveApprovedLocalMediaPath(symlinkInsideUserData)).resolves.toBeNull();
|
||||
@@ -173,6 +179,29 @@ describe("local media path policy", () => {
|
||||
expect(result.project).toMatchObject({ videoPath });
|
||||
});
|
||||
|
||||
it("rejects invalid project payloads before approving media paths", async () => {
|
||||
const downloadsPath = path.join(tempRoot, "Downloads");
|
||||
const videoPath = path.join(downloadsPath, "recording.mp4");
|
||||
const projectPath = path.join(tempPath, "invalid.recordly");
|
||||
await fs.mkdir(downloadsPath, { recursive: true });
|
||||
await fs.writeFile(videoPath, "test-video");
|
||||
await fs.writeFile(
|
||||
projectPath,
|
||||
JSON.stringify({
|
||||
videoPath,
|
||||
editor: {},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { loadProjectFromPath, resolveApprovedLocalMediaPath } = await import("./manager");
|
||||
|
||||
const result = await loadProjectFromPath(projectPath);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe("Invalid project file format");
|
||||
await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("approves editor audioRegions audioPath entries when loading a project", async () => {
|
||||
const downloadsPath = path.join(tempRoot, "Downloads");
|
||||
const videoPath = path.join(tempPath, "recording.mp4");
|
||||
@@ -187,9 +216,7 @@ describe("local media path policy", () => {
|
||||
version: 1,
|
||||
videoPath,
|
||||
editor: {
|
||||
audioRegions: [
|
||||
{ id: "a1", startMs: 0, endMs: 1000, audioPath, volume: 1 },
|
||||
],
|
||||
audioRegions: [{ id: "a1", startMs: 0, endMs: 1000, audioPath, volume: 1 }],
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
|
||||
@@ -403,6 +403,29 @@ export async function listProjectLibraryEntries() {
|
||||
};
|
||||
}
|
||||
|
||||
function isLoadableProjectData(projectData: unknown) {
|
||||
if (!projectData || typeof projectData !== "object" || Array.isArray(projectData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = projectData as {
|
||||
version?: unknown;
|
||||
projectId?: unknown;
|
||||
videoPath?: unknown;
|
||||
editor?: unknown;
|
||||
};
|
||||
|
||||
return (
|
||||
typeof candidate.version === "number" &&
|
||||
(candidate.projectId === undefined || typeof candidate.projectId === "string") &&
|
||||
typeof candidate.videoPath === "string" &&
|
||||
candidate.videoPath.trim().length > 0 &&
|
||||
candidate.editor != null &&
|
||||
typeof candidate.editor === "object" &&
|
||||
!Array.isArray(candidate.editor)
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadProjectFromPath(projectPath: string) {
|
||||
const normalizedPath = normalizePath(projectPath);
|
||||
let project: unknown;
|
||||
@@ -416,6 +439,13 @@ export async function loadProjectFromPath(projectPath: string) {
|
||||
message: `Failed to read project file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
if (!isLoadableProjectData(project)) {
|
||||
return {
|
||||
success: false,
|
||||
canceled: false,
|
||||
message: "Invalid project file format",
|
||||
};
|
||||
}
|
||||
const mediaSources = await resolveProjectMediaSources(project);
|
||||
|
||||
if (!mediaSources.success) {
|
||||
|
||||
@@ -64,4 +64,58 @@ describe("hasUnsavedProjectChanges", () => {
|
||||
|
||||
expect(hasUnsavedProjectChanges(current, createProjectData())).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores transient webcam media attachment changes", () => {
|
||||
const saved = createProjectData({
|
||||
editor: {
|
||||
...createProjectData().editor,
|
||||
webcam: {
|
||||
enabled: false,
|
||||
sourcePath: null,
|
||||
timeOffsetMs: 0,
|
||||
size: 28,
|
||||
},
|
||||
},
|
||||
});
|
||||
const current = createProjectData({
|
||||
editor: {
|
||||
...createProjectData().editor,
|
||||
webcam: {
|
||||
enabled: true,
|
||||
sourcePath: "/Users/test/webcam.mp4",
|
||||
timeOffsetMs: 125,
|
||||
size: 28,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(hasUnsavedProjectChanges(current, saved)).toBe(false);
|
||||
});
|
||||
|
||||
it("detects persistent webcam presentation changes", () => {
|
||||
const saved = createProjectData({
|
||||
editor: {
|
||||
...createProjectData().editor,
|
||||
webcam: {
|
||||
enabled: true,
|
||||
sourcePath: "/Users/test/webcam.mp4",
|
||||
timeOffsetMs: 0,
|
||||
size: 28,
|
||||
},
|
||||
},
|
||||
});
|
||||
const current = createProjectData({
|
||||
editor: {
|
||||
...createProjectData().editor,
|
||||
webcam: {
|
||||
enabled: true,
|
||||
sourcePath: "/Users/test/webcam.mp4",
|
||||
timeOffsetMs: 0,
|
||||
size: 36,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(hasUnsavedProjectChanges(current, saved)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user