mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
bcf2a51d41
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 -->
209 lines
4.8 KiB
TypeScript
209 lines
4.8 KiB
TypeScript
import { realpath } from 'node:fs/promises';
|
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
|
import type { EventBasedChannel } from 'async-call-rpc';
|
|
|
|
export function getTime() {
|
|
return Date.now();
|
|
}
|
|
|
|
export const isMacOS = () => {
|
|
return process.platform === 'darwin';
|
|
};
|
|
|
|
export const isWindows = () => {
|
|
return process.platform === 'win32';
|
|
};
|
|
|
|
export const isLinux = () => {
|
|
return process.platform === 'linux';
|
|
};
|
|
|
|
interface MessagePortLike {
|
|
postMessage: (data: unknown) => void;
|
|
addListener: (event: 'message', listener: (...args: any[]) => void) => void;
|
|
removeListener: (
|
|
event: 'message',
|
|
listener: (...args: any[]) => void
|
|
) => void;
|
|
}
|
|
|
|
export class MessageEventChannel implements EventBasedChannel {
|
|
constructor(private readonly worker: MessagePortLike) {}
|
|
|
|
on(listener: (data: unknown) => void) {
|
|
const f = (data: unknown) => {
|
|
listener(data);
|
|
};
|
|
this.worker.addListener('message', f);
|
|
return () => {
|
|
this.worker.removeListener('message', f);
|
|
};
|
|
}
|
|
|
|
send(data: unknown) {
|
|
this.worker.postMessage(data);
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
typeof objA !== 'object' ||
|
|
objA === null ||
|
|
typeof objB !== 'object' ||
|
|
objB === null
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const keysA = Object.keys(objA);
|
|
const keysB = Object.keys(objB);
|
|
|
|
if (keysA.length !== keysB.length) {
|
|
return false;
|
|
}
|
|
|
|
// Test for A's keys different from B.
|
|
for (const key of keysA) {
|
|
if (
|
|
!Object.prototype.hasOwnProperty.call(objB, key) ||
|
|
!Object.is(objA[key as keyof T], objB[key as keyof T])
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|