fix(core): svg blob syncing issue (#4886)
This commit is contained in:
@@ -9,6 +9,7 @@ import { fetcher } from '@affine/workspace/affine/gql';
|
|||||||
import type { BlobStorage } from '@blocksuite/store';
|
import type { BlobStorage } from '@blocksuite/store';
|
||||||
|
|
||||||
import { predefinedStaticFiles } from './local-static-storage';
|
import { predefinedStaticFiles } from './local-static-storage';
|
||||||
|
import { bufferToBlob } from './util';
|
||||||
|
|
||||||
export const createCloudBlobStorage = (workspaceId: string): BlobStorage => {
|
export const createCloudBlobStorage = (workspaceId: string): BlobStorage => {
|
||||||
return {
|
return {
|
||||||
@@ -17,15 +18,15 @@ export const createCloudBlobStorage = (workspaceId: string): BlobStorage => {
|
|||||||
const suffix = predefinedStaticFiles.includes(key)
|
const suffix = predefinedStaticFiles.includes(key)
|
||||||
? `/static/${key}`
|
? `/static/${key}`
|
||||||
: `/api/workspaces/${workspaceId}/blobs/${key}`;
|
: `/api/workspaces/${workspaceId}/blobs/${key}`;
|
||||||
|
|
||||||
return fetchWithTraceReport(
|
return fetchWithTraceReport(
|
||||||
runtimeConfig.serverUrlPrefix + suffix
|
runtimeConfig.serverUrlPrefix + suffix
|
||||||
).then(res => {
|
).then(async res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// status not in the range 200-299
|
// status not in the range 200-299
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// todo: shall we add svg type here if it is missing?
|
return bufferToBlob(await res.arrayBuffer());
|
||||||
return res.blob();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
set: async (key, value) => {
|
set: async (key, value) => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { BlobStorage } from '@blocksuite/store';
|
import type { BlobStorage } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import { bufferToBlob } from './util';
|
||||||
|
|
||||||
export const predefinedStaticFiles = [
|
export const predefinedStaticFiles = [
|
||||||
'029uztLz2CzJezK7UUhrbGiWUdZ0J7NVs_qR6RDsvb8=',
|
'029uztLz2CzJezK7UUhrbGiWUdZ0J7NVs_qR6RDsvb8=',
|
||||||
'047ebf2c9a5c7c9d8521c2ea5e6140ff7732ef9e28a9f944e9bf3ca4',
|
'047ebf2c9a5c7c9d8521c2ea5e6140ff7732ef9e28a9f944e9bf3ca4',
|
||||||
@@ -40,17 +42,21 @@ export const createStaticStorage = (): BlobStorage => {
|
|||||||
return {
|
return {
|
||||||
crud: {
|
crud: {
|
||||||
get: async (key: string) => {
|
get: async (key: string) => {
|
||||||
if (key.startsWith('/static/')) {
|
const isStaticResource =
|
||||||
const response = await fetch(key);
|
predefinedStaticFiles.includes(key) || key.startsWith('/static/');
|
||||||
if (response.ok) {
|
|
||||||
return response.blob();
|
if (!isStaticResource) {
|
||||||
}
|
return null;
|
||||||
} else if (predefinedStaticFiles.includes(key)) {
|
|
||||||
const response = await fetch(`/static/${key}`);
|
|
||||||
if (response.ok) {
|
|
||||||
return response.blob();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
set: async (key: string) => {
|
set: async (key: string) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { BlobStorage } from '@blocksuite/store';
|
import type { BlobStorage } from '@blocksuite/store';
|
||||||
|
|
||||||
import { isSvgBuffer } from './util';
|
import { bufferToBlob } from './util';
|
||||||
|
|
||||||
export const createSQLiteStorage = (workspaceId: string): BlobStorage => {
|
export const createSQLiteStorage = (workspaceId: string): BlobStorage => {
|
||||||
const apis = window.apis;
|
const apis = window.apis;
|
||||||
@@ -11,11 +11,7 @@ export const createSQLiteStorage = (workspaceId: string): BlobStorage => {
|
|||||||
get: async (key: string) => {
|
get: async (key: string) => {
|
||||||
const buffer = await apis.db.getBlob(workspaceId, key);
|
const buffer = await apis.db.getBlob(workspaceId, key);
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
const isSVG = isSvgBuffer(buffer);
|
return bufferToBlob(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 null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,3 +7,13 @@ export function isSvgBuffer(buffer: Uint8Array) {
|
|||||||
const str = decoder.decode(buffer);
|
const str = decoder.decode(buffer);
|
||||||
return isSvg(str);
|
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]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
enableCloudWorkspaceFromShareButton,
|
enableCloudWorkspaceFromShareButton,
|
||||||
loginUser,
|
loginUser,
|
||||||
} from '@affine-test/kit/utils/cloud';
|
} from '@affine-test/kit/utils/cloud';
|
||||||
|
import { dropFile } from '@affine-test/kit/utils/drop-file';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
@@ -284,3 +285,55 @@ test.describe('sign out', () => {
|
|||||||
expect(page.url()).toBe(currentUrl);
|
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
||||||
|
<rect x="0" y="0" width="200" height="200" fill="red" />
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
35
tests/kit/utils/drop-file.ts
Normal file
35
tests/kit/utils/drop-file.ts
Normal file
@@ -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 });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user