From 702dbf7be4eee4fa0a32a6595a7dd6525863767f Mon Sep 17 00:00:00 2001
From: DarkSky <25152247+darkskygit@users.noreply.github.com>
Date: Sat, 27 Dec 2025 17:56:42 +0800
Subject: [PATCH] fix: client indexing & outdated scheme (#14160)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary by CodeRabbit
* **New Features**
* Optimized storage handling with platform-specific
implementations—SQLite for Electron and IndexedDB for other environments
for improved performance.
* **Bug Fixes**
* Enhanced recording file access and retrieval functionality for better
reliability.
* Strengthened local file protocol handling and security restrictions.
✏️ Tip: You can customize this high-level summary in your review
settings.
---
.../src/app/effects/recording.ts | 4 +-
.../apps/electron/src/main/protocol.ts | 21 +----
.../src/main/security-restrictions.ts | 4 -
.../core/src/modules/navigation/utils.ts | 1 -
.../modules/workspace-engine/impls/cloud.ts | 7 +-
.../frontend/core/src/utils/opus-encoding.ts | 94 +++++++++++++------
tools/cli/src/webpack/index.ts | 11 +++
7 files changed, 85 insertions(+), 57 deletions(-)
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 57bae90b8..79f07d06a 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 ba986160e..f2952c486 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 1cbf91a0d..e42623ef2 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 540e537e0..6eaa94942 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 500638678..56672c53c 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 27d8e1107..abfea31b0 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 05028b0de..36b9a5854 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'],