Remove backend import & export features
This commit is contained in:
parent
36a819491a
commit
f1a1c4b28e
@ -55,7 +55,6 @@ import Collab, {
|
|||||||
isOfflineAtom,
|
isOfflineAtom,
|
||||||
} from "./collab/Collab";
|
} from "./collab/Collab";
|
||||||
import {
|
import {
|
||||||
exportToBackend,
|
|
||||||
getCollaborationLinkData,
|
getCollaborationLinkData,
|
||||||
isCollaborationLink,
|
isCollaborationLink,
|
||||||
loadScene,
|
loadScene,
|
||||||
@ -663,36 +662,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (exportedElements.length === 0) {
|
if (exportedElements.length === 0) {
|
||||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
try {
|
// Not supported as of now
|
||||||
const { url, errorMessage } = await exportToBackend(
|
throw new Error(t("alerts.couldNotCreateShareableLink"));
|
||||||
exportedElements,
|
|
||||||
{
|
|
||||||
...appState,
|
|
||||||
viewBackgroundColor: appState.exportBackground
|
|
||||||
? appState.viewBackgroundColor
|
|
||||||
: getDefaultAppState().viewBackgroundColor,
|
|
||||||
},
|
|
||||||
files,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (errorMessage) {
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
setLatestShareableLink(url);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name !== "AbortError") {
|
|
||||||
const { width, height } = appState;
|
|
||||||
console.error(error, {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
devicePixelRatio: window.devicePixelRatio,
|
|
||||||
});
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCustomStats = (
|
const renderCustomStats = (
|
||||||
|
|||||||
@ -1,28 +1,15 @@
|
|||||||
import {
|
import { generateEncryptionKey } from "../../packages/excalidraw/data/encryption";
|
||||||
compressData,
|
|
||||||
decompressData,
|
|
||||||
} from "../../packages/excalidraw/data/encode";
|
|
||||||
import {
|
|
||||||
decryptData,
|
|
||||||
generateEncryptionKey,
|
|
||||||
IV_LENGTH_BYTES,
|
|
||||||
} from "../../packages/excalidraw/data/encryption";
|
|
||||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
|
||||||
import { restore } from "../../packages/excalidraw/data/restore";
|
import { restore } from "../../packages/excalidraw/data/restore";
|
||||||
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { t } from "../../packages/excalidraw/i18n";
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
|
||||||
BinaryFiles,
|
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
@ -31,11 +18,8 @@ import { bytesToHexString } from "../../packages/excalidraw/utils";
|
|||||||
import type { WS_SUBTYPES } from "../app_constants";
|
import type { WS_SUBTYPES } from "../app_constants";
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
|
||||||
ROOM_ID_BYTES,
|
ROOM_ID_BYTES,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "./FileManager";
|
|
||||||
import { saveFilesToDatabase } from "./database";
|
|
||||||
|
|
||||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||||
MakeBrand<"SyncableExcalidrawElement">;
|
MakeBrand<"SyncableExcalidrawElement">;
|
||||||
@ -59,9 +43,6 @@ export const getSyncableElements = (
|
|||||||
isSyncableElement(element),
|
isSyncableElement(element),
|
||||||
) as SyncableExcalidrawElement[];
|
) as SyncableExcalidrawElement[];
|
||||||
|
|
||||||
const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
|
|
||||||
const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
|
|
||||||
|
|
||||||
const generateRoomId = async () => {
|
const generateRoomId = async () => {
|
||||||
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
const buffer = new Uint8Array(ROOM_ID_BYTES);
|
||||||
window.crypto.getRandomValues(buffer);
|
window.crypto.getRandomValues(buffer);
|
||||||
@ -160,84 +141,6 @@ export const getCollaborationLink = (data: {
|
|||||||
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes shareLink data using the legacy buffer format.
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
const legacy_decodeFromBackend = async ({
|
|
||||||
buffer,
|
|
||||||
decryptionKey,
|
|
||||||
}: {
|
|
||||||
buffer: ArrayBuffer;
|
|
||||||
decryptionKey: string;
|
|
||||||
}) => {
|
|
||||||
let decrypted: ArrayBuffer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Buffer should contain both the IV (fixed length) and encrypted data
|
|
||||||
const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
|
||||||
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
|
||||||
decrypted = await decryptData(new Uint8Array(iv), encrypted, decryptionKey);
|
|
||||||
} catch (error: any) {
|
|
||||||
// Fixed IV (old format, backward compatibility)
|
|
||||||
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
|
||||||
decrypted = await decryptData(fixedIv, buffer, decryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to convert the decrypted array buffer to a string
|
|
||||||
const string = new window.TextDecoder("utf-8").decode(
|
|
||||||
new Uint8Array(decrypted),
|
|
||||||
);
|
|
||||||
const data: ImportedDataState = JSON.parse(string);
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements: data.elements || null,
|
|
||||||
appState: data.appState || null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const importFromBackend = async (
|
|
||||||
id: string,
|
|
||||||
decryptionKey: string,
|
|
||||||
): Promise<ImportedDataState> => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${BACKEND_V2_GET}${id}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
window.alert(t("alerts.importBackendFailed"));
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: decodedBuffer } = await decompressData(
|
|
||||||
new Uint8Array(buffer),
|
|
||||||
{
|
|
||||||
decryptionKey,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const data: ImportedDataState = JSON.parse(
|
|
||||||
new TextDecoder().decode(decodedBuffer),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements: data.elements || null,
|
|
||||||
appState: data.appState || null,
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
console.warn(
|
|
||||||
"error when decoding shareLink data using the new format:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
return legacy_decodeFromBackend({ buffer, decryptionKey });
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
window.alert(t("alerts.importBackendFailed"));
|
|
||||||
console.error(error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadScene = async (
|
export const loadScene = async (
|
||||||
id: string | null,
|
id: string | null,
|
||||||
privateKey: string | null,
|
privateKey: string | null,
|
||||||
@ -247,20 +150,9 @@ export const loadScene = async (
|
|||||||
localDataState: ImportedDataState | undefined | null,
|
localDataState: ImportedDataState | undefined | null,
|
||||||
) => {
|
) => {
|
||||||
let data;
|
let data;
|
||||||
if (id != null && privateKey != null) {
|
data = restore(localDataState || null, null, null, {
|
||||||
// the private key is used to decrypt the content from the server, take
|
repairBindings: true,
|
||||||
// extra care not to leak it
|
});
|
||||||
data = restore(
|
|
||||||
await importFromBackend(id, privateKey),
|
|
||||||
localDataState?.appState,
|
|
||||||
localDataState?.elements,
|
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
data = restore(localDataState || null, null, null, {
|
|
||||||
repairBindings: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: data.elements,
|
elements: data.elements,
|
||||||
@ -271,68 +163,3 @@ export const loadScene = async (
|
|||||||
files: data.files,
|
files: data.files,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExportToBackendResult =
|
|
||||||
| { url: null; errorMessage: string }
|
|
||||||
| { url: string; errorMessage: null };
|
|
||||||
|
|
||||||
export const exportToBackend = async (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: Partial<AppState>,
|
|
||||||
files: BinaryFiles,
|
|
||||||
): Promise<ExportToBackendResult> => {
|
|
||||||
const encryptionKey = await generateEncryptionKey("string");
|
|
||||||
|
|
||||||
const payload = await compressData(
|
|
||||||
new TextEncoder().encode(
|
|
||||||
serializeAsJSON(elements, appState, files, "database"),
|
|
||||||
),
|
|
||||||
{ encryptionKey },
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filesMap = new Map<FileId, BinaryFileData>();
|
|
||||||
for (const element of elements) {
|
|
||||||
if (isInitializedImageElement(element) && files[element.fileId]) {
|
|
||||||
filesMap.set(element.fileId, files[element.fileId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesToUpload = await encodeFilesForUpload({
|
|
||||||
files: filesMap,
|
|
||||||
encryptionKey,
|
|
||||||
maxBytes: FILE_UPLOAD_MAX_BYTES,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(BACKEND_V2_POST, {
|
|
||||||
method: "POST",
|
|
||||||
body: payload.buffer,
|
|
||||||
});
|
|
||||||
const json = await response.json();
|
|
||||||
if (json.id) {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
// We need to store the key (and less importantly the id) as hash instead
|
|
||||||
// of queryParam in order to never send it to the server
|
|
||||||
url.hash = `json=${json.id},${encryptionKey}`;
|
|
||||||
const urlString = url.toString();
|
|
||||||
|
|
||||||
await saveFilesToDatabase({
|
|
||||||
prefix: `/files/shareLinks/${json.id}`,
|
|
||||||
files: filesToUpload,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { url: urlString, errorMessage: null };
|
|
||||||
} else if (json.error_class === "RequestTooLargeError") {
|
|
||||||
return {
|
|
||||||
url: null,
|
|
||||||
errorMessage: t("alerts.couldNotCreateShareableLinkTooBig"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user