mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-11 14:08:55 +08:00
fix: client indexing & outdated scheme (#14160)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -28,7 +28,9 @@ async function readRecordingFile(filepath: string) {
|
|||||||
|
|
||||||
const fileUrl = new URL(
|
const fileUrl = new URL(
|
||||||
filepath,
|
filepath,
|
||||||
location.origin.replace(/\.$/, 'local-file')
|
typeof location !== 'undefined' && location.protocol === 'assets:'
|
||||||
|
? 'assets://local-file'
|
||||||
|
: location.origin
|
||||||
);
|
);
|
||||||
const response = await fetch(fileUrl);
|
const response = await fetch(fileUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -20,16 +20,6 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
scheme: 'file',
|
|
||||||
privileges: {
|
|
||||||
secure: false,
|
|
||||||
corsEnabled: true,
|
|
||||||
supportFetchAPI: true,
|
|
||||||
standard: true,
|
|
||||||
stream: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const webStaticDir = join(resourcesPath, 'web-static');
|
const webStaticDir = join(resourcesPath, 'web-static');
|
||||||
@@ -152,10 +142,6 @@ function ensureFrameAncestors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerProtocol() {
|
export function registerProtocol() {
|
||||||
protocol.handle('file', request => {
|
|
||||||
return handleFileRequest(request);
|
|
||||||
});
|
|
||||||
|
|
||||||
protocol.handle('assets', request => {
|
protocol.handle('assets', request => {
|
||||||
return handleFileRequest(request);
|
return handleFileRequest(request);
|
||||||
});
|
});
|
||||||
@@ -202,15 +188,12 @@ export function registerProtocol() {
|
|||||||
|
|
||||||
const { protocol } = new URL(url);
|
const { protocol } = new URL(url);
|
||||||
|
|
||||||
// Only adjust CORS for assets/file responses; leave remote http(s) headers intact
|
// Only adjust CORS for assets responses; leave remote http(s) headers intact
|
||||||
if (protocol === 'assets:' || protocol === 'file:') {
|
if (protocol === 'assets:') {
|
||||||
delete responseHeaders['access-control-allow-origin'];
|
delete responseHeaders['access-control-allow-origin'];
|
||||||
delete responseHeaders['access-control-allow-headers'];
|
delete responseHeaders['access-control-allow-headers'];
|
||||||
delete responseHeaders['Access-Control-Allow-Origin'];
|
delete responseHeaders['Access-Control-Allow-Origin'];
|
||||||
delete responseHeaders['Access-Control-Allow-Headers'];
|
delete responseHeaders['Access-Control-Allow-Headers'];
|
||||||
}
|
|
||||||
|
|
||||||
if (protocol === 'assets:' || protocol === 'file:') {
|
|
||||||
setHeader(responseHeaders, 'X-Frame-Options', 'SAMEORIGIN');
|
setHeader(responseHeaders, 'X-Frame-Options', 'SAMEORIGIN');
|
||||||
ensureFrameAncestors(responseHeaders, "'self'");
|
ensureFrameAncestors(responseHeaders, "'self'");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ app.on('web-contents-created', (_, contents) => {
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (parsed.protocol === 'file:' && parsed.hostname === mainHost) {
|
|
||||||
// legacy allowance for older file:// loads
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch {}
|
} catch {}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import queryString from 'query-string';
|
|||||||
|
|
||||||
function maybeAffineOrigin(origin: string, baseUrl: string) {
|
function maybeAffineOrigin(origin: string, baseUrl: string) {
|
||||||
return (
|
return (
|
||||||
origin.startsWith('file://') ||
|
|
||||||
origin.startsWith('assets://') ||
|
origin.startsWith('assets://') ||
|
||||||
origin.endsWith('affine.pro') || // stable/beta
|
origin.endsWith('affine.pro') || // stable/beta
|
||||||
origin.endsWith('apple.getaffineapp.com') || // stable/beta
|
origin.endsWith('apple.getaffineapp.com') || // stable/beta
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
IndexedDBBlobSyncStorage,
|
IndexedDBBlobSyncStorage,
|
||||||
IndexedDBDocStorage,
|
IndexedDBDocStorage,
|
||||||
IndexedDBDocSyncStorage,
|
IndexedDBDocSyncStorage,
|
||||||
|
IndexedDBIndexerStorage,
|
||||||
} from '@affine/nbstore/idb';
|
} from '@affine/nbstore/idb';
|
||||||
import {
|
import {
|
||||||
IndexedDBV1BlobStorage,
|
IndexedDBV1BlobStorage,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
SqliteBlobSyncStorage,
|
SqliteBlobSyncStorage,
|
||||||
SqliteDocStorage,
|
SqliteDocStorage,
|
||||||
SqliteDocSyncStorage,
|
SqliteDocSyncStorage,
|
||||||
|
SqliteIndexerStorage,
|
||||||
} from '@affine/nbstore/sqlite';
|
} from '@affine/nbstore/sqlite';
|
||||||
import {
|
import {
|
||||||
SqliteV1BlobStorage,
|
SqliteV1BlobStorage,
|
||||||
@@ -130,6 +132,9 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
|||||||
BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid
|
BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid
|
||||||
? SqliteBlobSyncStorage
|
? SqliteBlobSyncStorage
|
||||||
: IndexedDBBlobSyncStorage;
|
: IndexedDBBlobSyncStorage;
|
||||||
|
IndexerStorageType = BUILD_CONFIG.isElectron
|
||||||
|
? SqliteIndexerStorage
|
||||||
|
: IndexedDBIndexerStorage;
|
||||||
|
|
||||||
async deleteWorkspace(id: string): Promise<void> {
|
async deleteWorkspace(id: string): Promise<void> {
|
||||||
await this.graphqlService.gql({
|
await this.graphqlService.gql({
|
||||||
@@ -481,7 +486,7 @@ class CloudWorkspaceFlavourProvider implements WorkspaceFlavourProvider {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
indexer: {
|
indexer: {
|
||||||
name: 'IndexedDBIndexerStorage',
|
name: this.IndexerStorageType.identifier,
|
||||||
opts: {
|
opts: {
|
||||||
flavour: this.flavour,
|
flavour: this.flavour,
|
||||||
type: 'workspace',
|
type: 'workspace',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { DebugLogger } from '@affine/debug';
|
|||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { ArrayBufferTarget, Muxer } from 'mp4-muxer';
|
import { ArrayBufferTarget, Muxer } from 'mp4-muxer';
|
||||||
|
|
||||||
|
import { isLink } from '../modules/navigation/utils';
|
||||||
|
|
||||||
interface AudioEncodingConfig {
|
interface AudioEncodingConfig {
|
||||||
sampleRate: number;
|
sampleRate: number;
|
||||||
numberOfChannels: number;
|
numberOfChannels: number;
|
||||||
@@ -14,6 +16,7 @@ interface AudioEncodingResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logger = new DebugLogger('opus-encoding');
|
const logger = new DebugLogger('opus-encoding');
|
||||||
|
const LOCAL_FILE_ASSET_URL = 'assets://local-file';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const DEFAULT_BITRATE = 64000;
|
const DEFAULT_BITRATE = 64000;
|
||||||
@@ -30,12 +33,61 @@ async function blobToArrayBuffer(
|
|||||||
if (blob instanceof Blob) {
|
if (blob instanceof Blob) {
|
||||||
return await blob.arrayBuffer();
|
return await blob.arrayBuffer();
|
||||||
} else if (blob instanceof Uint8Array) {
|
} else if (blob instanceof Uint8Array) {
|
||||||
return blob.buffer instanceof ArrayBuffer
|
return toArrayBuffer(blob);
|
||||||
? blob.buffer
|
|
||||||
: blob.slice().buffer;
|
|
||||||
} else {
|
|
||||||
return 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<ArrayBuffer> {
|
||||||
|
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;
|
encoder: AudioEncoder;
|
||||||
encodedChunks: EncodedAudioChunk[];
|
encodedChunks: EncodedAudioChunk[];
|
||||||
} {
|
} {
|
||||||
|
if (typeof AudioEncoder === 'undefined') {
|
||||||
|
throw new Error('AudioEncoder is not available in this environment');
|
||||||
|
}
|
||||||
|
|
||||||
const encodedChunks: EncodedAudioChunk[] = [];
|
const encodedChunks: EncodedAudioChunk[] = [];
|
||||||
const encoder = new AudioEncoder({
|
const encoder = new AudioEncoder({
|
||||||
output: chunk => {
|
output: chunk => {
|
||||||
@@ -198,38 +254,14 @@ export async function encodeRawBufferToOpus({
|
|||||||
numberOfChannels: number;
|
numberOfChannels: number;
|
||||||
}): Promise<Uint8Array> {
|
}): Promise<Uint8Array> {
|
||||||
logger.debug('Encoding raw buffer to Opus');
|
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({
|
const { encoder, encodedChunks } = createOpusEncoder({
|
||||||
sampleRate,
|
sampleRate,
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process the stream
|
const rawBuffer = await readRecordingFileBuffer(filepath);
|
||||||
const reader = response.body.getReader();
|
const audioData = new Float32Array(rawBuffer);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
await encodeAudioFrames({
|
await encodeAudioFrames({
|
||||||
audioData,
|
audioData,
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ export function createHTMLTargetConfig(
|
|||||||
|
|
||||||
const buildConfig = getBuildConfigFromEnv(pkg);
|
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 = {
|
const config: webpack.Configuration = {
|
||||||
//#region basic webpack config
|
//#region basic webpack config
|
||||||
name: entry['index'],
|
name: entry['index'],
|
||||||
|
|||||||
Reference in New Issue
Block a user