mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 01:26:37 +08:00
feat(native): record encoding (#14188)
fix #13784 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Start/stop system or meeting recordings with Ogg/Opus artifacts and native start/stop APIs; workspace backup recovery. * **Refactor** * Simplified recording lifecycle and UI flows; native runtime now orchestrates recording/processing and reporting. * **Bug Fixes** * Stronger path validation, safer import/export dialogs, consistent error handling/logging, and retry-safe recording processing. * **Chores** * Added cross-platform native audio capture and Ogg/Opus encoding support. * **Tests** * New unit, integration, and e2e tests for recording, path guards, dialogs, and workspace recovery. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { join } from 'node:path';
|
||||
import { realpath } from 'node:fs/promises';
|
||||
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
|
||||
import type { EventBasedChannel } from 'async-call-rpc';
|
||||
|
||||
@@ -47,6 +48,130 @@ export class MessageEventChannel implements EventBasedChannel {
|
||||
|
||||
export const resourcesPath = join(__dirname, `../resources`);
|
||||
|
||||
function normalizeComparedPath(path: string, caseInsensitive: boolean) {
|
||||
return caseInsensitive ? path.toLowerCase() : path;
|
||||
}
|
||||
|
||||
export function isPathInsideBase(
|
||||
basePath: string,
|
||||
targetPath: string,
|
||||
options: { caseInsensitive?: boolean } = {}
|
||||
) {
|
||||
const { caseInsensitive = false } = options;
|
||||
const normalizedBase = normalizeComparedPath(
|
||||
resolve(basePath),
|
||||
caseInsensitive
|
||||
);
|
||||
const normalizedTarget = normalizeComparedPath(
|
||||
resolve(targetPath),
|
||||
caseInsensitive
|
||||
);
|
||||
const rel = relative(normalizedBase, normalizedTarget);
|
||||
|
||||
return (
|
||||
rel === '' ||
|
||||
(!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`))
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePathInBase(
|
||||
basePath: string,
|
||||
targetPath: string,
|
||||
options: { caseInsensitive?: boolean; label?: string } = {}
|
||||
) {
|
||||
const resolvedBase = resolve(basePath);
|
||||
const resolvedTarget = resolve(resolvedBase, targetPath);
|
||||
|
||||
if (!isPathInsideBase(resolvedBase, resolvedTarget, options)) {
|
||||
throw new Error(
|
||||
options.label ? `Invalid ${options.label}` : 'Invalid path'
|
||||
);
|
||||
}
|
||||
|
||||
return resolvedTarget;
|
||||
}
|
||||
|
||||
export async function resolveExistingPath(targetPath: string) {
|
||||
try {
|
||||
return await realpath(targetPath);
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
||||
return resolve(targetPath);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveExistingPathInBase(
|
||||
basePath: string,
|
||||
targetPath: string,
|
||||
options: { caseInsensitive?: boolean; label?: string } = {}
|
||||
) {
|
||||
const [resolvedBase, resolvedTarget] = await Promise.all([
|
||||
resolveExistingPath(basePath),
|
||||
resolveExistingPath(targetPath),
|
||||
]);
|
||||
|
||||
if (!isPathInsideBase(resolvedBase, resolvedTarget, options)) {
|
||||
throw new Error(
|
||||
options.label ? `Invalid ${options.label}` : 'Invalid path'
|
||||
);
|
||||
}
|
||||
|
||||
return resolvedTarget;
|
||||
}
|
||||
|
||||
export function assertPathComponent(
|
||||
value: string,
|
||||
label: string = 'path component'
|
||||
) {
|
||||
const hasControlChar = Array.from(value).some(
|
||||
character => character.charCodeAt(0) < 0x20
|
||||
);
|
||||
|
||||
if (
|
||||
!value ||
|
||||
value === '.' ||
|
||||
value === '..' ||
|
||||
/[/\\]/.test(value) ||
|
||||
hasControlChar
|
||||
) {
|
||||
throw new Error(`Invalid ${label}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function normalizeWorkspaceIdForPath(
|
||||
value: string,
|
||||
options: { windows?: boolean; label?: string } = {}
|
||||
) {
|
||||
const { windows = isWindows(), label = 'workspace id' } = options;
|
||||
const safeValue = assertPathComponent(value, label);
|
||||
|
||||
if (!windows) {
|
||||
return safeValue;
|
||||
}
|
||||
|
||||
const windowsReservedChars = new Set(['<', '>', ':', '"', '|', '?', '*']);
|
||||
let normalized = '';
|
||||
|
||||
for (const character of safeValue) {
|
||||
normalized += windowsReservedChars.has(character) ? '_' : character;
|
||||
}
|
||||
|
||||
while (normalized.endsWith('.') || normalized.endsWith(' ')) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!normalized || normalized === '.' || normalized === '..') {
|
||||
throw new Error(`Invalid ${label}`);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
|
||||
export function shallowEqual<T>(objA: T, objB: T) {
|
||||
if (Object.is(objA, objB)) {
|
||||
|
||||
Reference in New Issue
Block a user