diff --git a/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts b/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts index 57bae90b89..79f07d06ac 100644 --- a/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts +++ b/packages/frontend/apps/electron-renderer/src/app/effects/recording.ts @@ -28,7 +28,9 @@ async function readRecordingFile(filepath: string) { const fileUrl = new URL( filepath, - location.origin.replace(/\.$/, 'local-file') + typeof location !== 'undefined' && location.protocol === 'assets:' + ? 'assets://local-file' + : location.origin ); const response = await fetch(fileUrl); if (!response.ok) { diff --git a/packages/frontend/apps/electron/src/main/protocol.ts b/packages/frontend/apps/electron/src/main/protocol.ts index ba986160eb..f2952c4869 100644 --- a/packages/frontend/apps/electron/src/main/protocol.ts +++ b/packages/frontend/apps/electron/src/main/protocol.ts @@ -20,16 +20,6 @@ protocol.registerSchemesAsPrivileged([ stream: true, }, }, - { - scheme: 'file', - privileges: { - secure: false, - corsEnabled: true, - supportFetchAPI: true, - standard: true, - stream: true, - }, - }, ]); const webStaticDir = join(resourcesPath, 'web-static'); @@ -152,10 +142,6 @@ function ensureFrameAncestors( } export function registerProtocol() { - protocol.handle('file', request => { - return handleFileRequest(request); - }); - protocol.handle('assets', request => { return handleFileRequest(request); }); @@ -202,15 +188,12 @@ export function registerProtocol() { const { protocol } = new URL(url); - // Only adjust CORS for assets/file responses; leave remote http(s) headers intact - if (protocol === 'assets:' || protocol === 'file:') { + // Only adjust CORS for assets responses; leave remote http(s) headers intact + if (protocol === 'assets:') { delete responseHeaders['access-control-allow-origin']; delete responseHeaders['access-control-allow-headers']; delete responseHeaders['Access-Control-Allow-Origin']; delete responseHeaders['Access-Control-Allow-Headers']; - } - - if (protocol === 'assets:' || protocol === 'file:') { setHeader(responseHeaders, 'X-Frame-Options', 'SAMEORIGIN'); ensureFrameAncestors(responseHeaders, "'self'"); } diff --git a/packages/frontend/apps/electron/src/main/security-restrictions.ts b/packages/frontend/apps/electron/src/main/security-restrictions.ts index 1cbf91a0df..e42623ef29 100644 --- a/packages/frontend/apps/electron/src/main/security-restrictions.ts +++ b/packages/frontend/apps/electron/src/main/security-restrictions.ts @@ -42,10 +42,6 @@ app.on('web-contents-created', (_, contents) => { ) { return true; } - if (parsed.protocol === 'file:' && parsed.hostname === mainHost) { - // legacy allowance for older file:// loads - return true; - } } catch {} return false; }; diff --git a/packages/frontend/core/src/modules/navigation/utils.ts b/packages/frontend/core/src/modules/navigation/utils.ts index 540e537e06..6eaa949422 100644 --- a/packages/frontend/core/src/modules/navigation/utils.ts +++ b/packages/frontend/core/src/modules/navigation/utils.ts @@ -6,7 +6,6 @@ import queryString from 'query-string'; function maybeAffineOrigin(origin: string, baseUrl: string) { return ( - origin.startsWith('file://') || origin.startsWith('assets://') || origin.endsWith('affine.pro') || // stable/beta origin.endsWith('apple.getaffineapp.com') || // stable/beta diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts index 5006386780..56672c53ce 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts @@ -19,6 +19,7 @@ import { IndexedDBBlobSyncStorage, IndexedDBDocStorage, IndexedDBDocSyncStorage, + IndexedDBIndexerStorage, } from '@affine/nbstore/idb'; import { IndexedDBV1BlobStorage, @@ -29,6 +30,7 @@ import { SqliteBlobSyncStorage, SqliteDocStorage, SqliteDocSyncStorage, + SqliteIndexerStorage, } from '@affine/nbstore/sqlite'; import { SqliteV1BlobStorage, @@ -130,6 +132,9 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid ? SqliteBlobSyncStorage : IndexedDBBlobSyncStorage; + IndexerStorageType = BUILD_CONFIG.isElectron + ? SqliteIndexerStorage + : IndexedDBIndexerStorage; async deleteWorkspace(id: string): Promise { await this.graphqlService.gql({ @@ -481,7 +486,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider { }, }, indexer: { - name: 'IndexedDBIndexerStorage', + name: this.IndexerStorageType.identifier, opts: { flavour: this.flavour, type: 'workspace', diff --git a/packages/frontend/core/src/utils/opus-encoding.ts b/packages/frontend/core/src/utils/opus-encoding.ts index 27d8e11074..abfea31b01 100644 --- a/packages/frontend/core/src/utils/opus-encoding.ts +++ b/packages/frontend/core/src/utils/opus-encoding.ts @@ -2,6 +2,8 @@ import { DebugLogger } from '@affine/debug'; import { apis } from '@affine/electron-api'; import { ArrayBufferTarget, Muxer } from 'mp4-muxer'; +import { isLink } from '../modules/navigation/utils'; + interface AudioEncodingConfig { sampleRate: number; numberOfChannels: number; @@ -14,6 +16,7 @@ interface AudioEncodingResult { } const logger = new DebugLogger('opus-encoding'); +const LOCAL_FILE_ASSET_URL = 'assets://local-file'; // Constants const DEFAULT_BITRATE = 64000; @@ -30,12 +33,61 @@ async function blobToArrayBuffer( if (blob instanceof Blob) { return await blob.arrayBuffer(); } else if (blob instanceof Uint8Array) { - return blob.buffer instanceof ArrayBuffer - ? blob.buffer - : blob.slice().buffer; - } else { - return blob; + return toArrayBuffer(blob); } + return toArrayBuffer(blob); +} + +function toArrayBuffer(data: ArrayBuffer | ArrayBufferView): ArrayBuffer { + if (data instanceof ArrayBuffer) { + return data; + } + return data.buffer.slice( + data.byteOffset, + data.byteOffset + data.byteLength + ) as ArrayBuffer; +} + +function getRecordingFileUrl(filepath: string): URL { + const base = + typeof location !== 'undefined' && location.protocol === 'assets:' + ? LOCAL_FILE_ASSET_URL + : typeof location !== 'undefined' + ? location.origin + : LOCAL_FILE_ASSET_URL; + + // If filepath already contains a protocol, use it directly + const fileUrl = isLink(filepath) + ? new URL(filepath) + : new URL(filepath, base); + + if (fileUrl.protocol === 'assets:') { + // Force requests to go through the local-file host so the protocol handler + // can validate paths correctly. + fileUrl.hostname = 'local-file'; + } + + return fileUrl; +} + +async function readRecordingFileBuffer(filepath: string): Promise { + if (apis?.recording?.readRecordingFile) { + try { + const buffer = await apis.recording.readRecordingFile(filepath); + return toArrayBuffer(buffer); + } catch (error) { + logger.error('Failed to read recording file via IPC', error); + } + } + + const response = await fetch(getRecordingFileUrl(filepath)); + if (!response.ok) { + throw new Error( + `Failed to fetch recording file: ${response.status} ${response.statusText}` + ); + } + + return await response.arrayBuffer(); } /** @@ -71,6 +123,10 @@ export function createOpusEncoder(config: AudioEncodingConfig): { encoder: AudioEncoder; encodedChunks: EncodedAudioChunk[]; } { + if (typeof AudioEncoder === 'undefined') { + throw new Error('AudioEncoder is not available in this environment'); + } + const encodedChunks: EncodedAudioChunk[] = []; const encoder = new AudioEncoder({ output: chunk => { @@ -198,38 +254,14 @@ export async function encodeRawBufferToOpus({ numberOfChannels: number; }): Promise { logger.debug('Encoding raw buffer to Opus'); - const response = await fetch(new URL(filepath, location.origin)); - if (!response.body) { - throw new Error('Response body is null'); - } const { encoder, encodedChunks } = createOpusEncoder({ sampleRate, numberOfChannels, }); - // Process the stream - const reader = response.body.getReader(); - const chunks: Float32Array[] = []; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(new Float32Array(value.buffer)); - } - } finally { - reader.releaseLock(); - } - - // Combine all chunks into a single Float32Array - const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); - const audioData = new Float32Array(totalLength); - let offset = 0; - for (const chunk of chunks) { - audioData.set(chunk, offset); - offset += chunk.length; - } + const rawBuffer = await readRecordingFileBuffer(filepath); + const audioData = new Float32Array(rawBuffer); await encodeAudioFrames({ audioData, diff --git a/tools/cli/src/webpack/index.ts b/tools/cli/src/webpack/index.ts index 05028b0de2..36b9a58546 100644 --- a/tools/cli/src/webpack/index.ts +++ b/tools/cli/src/webpack/index.ts @@ -63,6 +63,17 @@ export function createHTMLTargetConfig( const buildConfig = getBuildConfigFromEnv(pkg); + console.log( + `Building [${pkg.name}] for [${buildConfig.appBuildType}] channel in [${buildConfig.debug ? 'development' : 'production'}] mode.` + ); + console.log( + `Entry points: ${Object.entries(entry) + .map(([name, path]) => `${name}: ${path}`) + .join(', ')}` + ); + console.log(`Output path: ${pkg.distPath.value}`); + console.log(`Config: ${JSON.stringify(buildConfig, null, 2)}`); + const config: webpack.Configuration = { //#region basic webpack config name: entry['index'],