diff --git a/packages/frontend/workspace/src/blob/cloud-blob-storage.ts b/packages/frontend/workspace/src/blob/cloud-blob-storage.ts index 44ef6c05db..7a8ca39549 100644 --- a/packages/frontend/workspace/src/blob/cloud-blob-storage.ts +++ b/packages/frontend/workspace/src/blob/cloud-blob-storage.ts @@ -9,6 +9,7 @@ import { fetcher } from '@affine/workspace/affine/gql'; import type { BlobStorage } from '@blocksuite/store'; import { predefinedStaticFiles } from './local-static-storage'; +import { bufferToBlob } from './util'; export const createCloudBlobStorage = (workspaceId: string): BlobStorage => { return { @@ -17,15 +18,15 @@ export const createCloudBlobStorage = (workspaceId: string): BlobStorage => { const suffix = predefinedStaticFiles.includes(key) ? `/static/${key}` : `/api/workspaces/${workspaceId}/blobs/${key}`; + return fetchWithTraceReport( runtimeConfig.serverUrlPrefix + suffix - ).then(res => { + ).then(async res => { if (!res.ok) { // status not in the range 200-299 return null; } - // todo: shall we add svg type here if it is missing? - return res.blob(); + return bufferToBlob(await res.arrayBuffer()); }); }, set: async (key, value) => { diff --git a/packages/frontend/workspace/src/blob/local-static-storage.ts b/packages/frontend/workspace/src/blob/local-static-storage.ts index 647e276040..93c3bf79c8 100644 --- a/packages/frontend/workspace/src/blob/local-static-storage.ts +++ b/packages/frontend/workspace/src/blob/local-static-storage.ts @@ -1,5 +1,7 @@ import type { BlobStorage } from '@blocksuite/store'; +import { bufferToBlob } from './util'; + export const predefinedStaticFiles = [ '029uztLz2CzJezK7UUhrbGiWUdZ0J7NVs_qR6RDsvb8=', '047ebf2c9a5c7c9d8521c2ea5e6140ff7732ef9e28a9f944e9bf3ca4', @@ -40,17 +42,21 @@ export const createStaticStorage = (): BlobStorage => { return { crud: { get: async (key: string) => { - if (key.startsWith('/static/')) { - const response = await fetch(key); - if (response.ok) { - return response.blob(); - } - } else if (predefinedStaticFiles.includes(key)) { - const response = await fetch(`/static/${key}`); - if (response.ok) { - return response.blob(); - } + const isStaticResource = + predefinedStaticFiles.includes(key) || key.startsWith('/static/'); + + if (!isStaticResource) { + return null; } + + const path = key.startsWith('/static/') ? key : `/static/${key}`; + const response = await fetch(path); + + if (response.ok) { + const buffer = await response.arrayBuffer(); + return bufferToBlob(buffer); + } + return null; }, set: async (key: string) => { diff --git a/packages/frontend/workspace/src/blob/sqlite-blob-storage.ts b/packages/frontend/workspace/src/blob/sqlite-blob-storage.ts index fafe15995c..c095eeee33 100644 --- a/packages/frontend/workspace/src/blob/sqlite-blob-storage.ts +++ b/packages/frontend/workspace/src/blob/sqlite-blob-storage.ts @@ -1,7 +1,7 @@ import { assertExists } from '@blocksuite/global/utils'; import type { BlobStorage } from '@blocksuite/store'; -import { isSvgBuffer } from './util'; +import { bufferToBlob } from './util'; export const createSQLiteStorage = (workspaceId: string): BlobStorage => { const apis = window.apis; @@ -11,11 +11,7 @@ export const createSQLiteStorage = (workspaceId: string): BlobStorage => { get: async (key: string) => { const buffer = await apis.db.getBlob(workspaceId, key); if (buffer) { - const isSVG = isSvgBuffer(buffer); - // for svg blob, we need to explicitly set the type to image/svg+xml - return isSVG - ? new Blob([buffer], { type: 'image/svg+xml' }) - : new Blob([buffer]); + return bufferToBlob(buffer); } return null; }, diff --git a/packages/frontend/workspace/src/blob/util.ts b/packages/frontend/workspace/src/blob/util.ts index 2ad3400803..e5c206b1fe 100644 --- a/packages/frontend/workspace/src/blob/util.ts +++ b/packages/frontend/workspace/src/blob/util.ts @@ -7,3 +7,13 @@ export function isSvgBuffer(buffer: Uint8Array) { const str = decoder.decode(buffer); return isSvg(str); } + +export function bufferToBlob(buffer: Uint8Array | ArrayBuffer) { + const isSVG = isSvgBuffer( + buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer + ); + // for svg blob, we need to explicitly set the type to image/svg+xml + return isSVG + ? new Blob([buffer], { type: 'image/svg+xml' }) + : new Blob([buffer]); +} diff --git a/tests/affine-cloud/e2e/collaboration.spec.ts b/tests/affine-cloud/e2e/collaboration.spec.ts index b1be89ea1a..a97af04e28 100644 --- a/tests/affine-cloud/e2e/collaboration.spec.ts +++ b/tests/affine-cloud/e2e/collaboration.spec.ts @@ -6,6 +6,7 @@ import { enableCloudWorkspaceFromShareButton, loginUser, } from '@affine-test/kit/utils/cloud'; +import { dropFile } from '@affine-test/kit/utils/drop-file'; import { clickNewPageButton, getBlockSuiteEditorTitle, @@ -284,3 +285,55 @@ test.describe('sign out', () => { expect(page.url()).toBe(currentUrl); }); }); + +test('can sync svg between different browsers', async ({ page, browser }) => { + await page.reload(); + await waitForEditorLoad(page); + await createLocalWorkspace( + { + name: 'test', + }, + page + ); + await enableCloudWorkspace(page); + await clickNewPageButton(page); + await waitForEditorLoad(page); + + // drop an svg file + const svg = ` + + `; + + await dropFile(page, 'affine-paragraph', svg, 'test.svg', 'image/svg+xml'); + + { + const context = await browser.newContext(); + const page2 = await context.newPage(); + await loginUser(page2, user.email); + await page2.goto(page.url()); + + // the user should see the svg + // get the image src under "affine-image img" + const src = await page2.locator('affine-image img').getAttribute('src'); + + expect(src).not.toBeNull(); + + // fetch the src resource in the browser + const svg2 = await page2.evaluate(async src => { + async function blobToString(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsText(blob); + }); + } + + const blob = fetch(src!).then(res => res.blob()); + return blobToString(await blob); + }, src); + + // turn the blob into string and check if it contains the svg + expect(svg2).toContain(svg); + } +}); diff --git a/tests/kit/utils/drop-file.ts b/tests/kit/utils/drop-file.ts new file mode 100644 index 0000000000..d8839efdb3 --- /dev/null +++ b/tests/kit/utils/drop-file.ts @@ -0,0 +1,35 @@ +import type { Page } from '@playwright/test'; + +export const dropFile = async ( + page: Page, + selector: string, + fileContent: Buffer | string, + fileName: string, + fileType = '' +) => { + const buffer = + typeof fileContent === 'string' + ? Buffer.from(fileContent, 'utf-8') + : fileContent; + + const dataTransfer = await page.evaluateHandle( + async ({ bufferData, localFileName, localFileType }) => { + const dt = new DataTransfer(); + + const blobData = await fetch(bufferData).then(res => res.blob()); + + const file = new File([blobData], localFileName, { type: localFileType }); + dt.items.add(file); + return dt; + }, + { + bufferData: `data:application/octet-stream;base64,${buffer.toString( + 'base64' + )}`, + localFileName: fileName, + localFileType: fileType, + } + ); + + await page.dispatchEvent(selector, 'drop', { dataTransfer }); +};