feat: Merge upstream/master into b310-digital/excalidraw#master (#3)
This commit is contained in:
@ -1,14 +1,15 @@
|
||||
import { StoreAction } from "../../packages/excalidraw";
|
||||
import { compressData } from "../../packages/excalidraw/data/encode";
|
||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||
import {
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import {
|
||||
import type {
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
ExcalidrawImperativeAPI,
|
||||
@ -238,5 +239,6 @@ export const updateStaleImageStatuses = (params: {
|
||||
}
|
||||
return element;
|
||||
}),
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
};
|
||||
|
@ -10,18 +10,29 @@
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||
import {
|
||||
createStore,
|
||||
entries,
|
||||
del,
|
||||
getMany,
|
||||
set,
|
||||
setMany,
|
||||
get,
|
||||
} from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
||||
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import type { MaybePromise } from "../../packages/excalidraw/utility-types";
|
||||
import { debounce } from "../../packages/excalidraw/utils";
|
||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||
import { FileManager } from "./FileManager";
|
||||
@ -183,3 +194,52 @@ export class LocalData {
|
||||
},
|
||||
});
|
||||
}
|
||||
export class LibraryIndexedDBAdapter {
|
||||
/** IndexedDB database and store name */
|
||||
private static idb_name = STORAGE_KEYS.IDB_LIBRARY;
|
||||
/** library data store key */
|
||||
private static key = "libraryData";
|
||||
|
||||
private static store = createStore(
|
||||
`${LibraryIndexedDBAdapter.idb_name}-db`,
|
||||
`${LibraryIndexedDBAdapter.idb_name}-store`,
|
||||
);
|
||||
|
||||
static async load() {
|
||||
const IDBData = await get<LibraryPersistedData>(
|
||||
LibraryIndexedDBAdapter.key,
|
||||
LibraryIndexedDBAdapter.store,
|
||||
);
|
||||
|
||||
return IDBData || null;
|
||||
}
|
||||
|
||||
static save(data: LibraryPersistedData): MaybePromise<void> {
|
||||
return set(
|
||||
LibraryIndexedDBAdapter.key,
|
||||
data,
|
||||
LibraryIndexedDBAdapter.store,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** LS Adapter used only for migrating LS library data
|
||||
* to indexedDB */
|
||||
export class LibraryLocalStorageMigrationAdapter {
|
||||
static load() {
|
||||
const LSData = localStorage.getItem(
|
||||
STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY,
|
||||
);
|
||||
if (LSData != null) {
|
||||
const libraryItems: ImportedDataState["libraryItems"] =
|
||||
JSON.parse(LSData);
|
||||
if (libraryItems) {
|
||||
return { libraryItems };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static clear() {
|
||||
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { SyncableExcalidrawElement } from ".";
|
||||
import { ExcalidrawElement, FileId } from "../../packages/excalidraw/element/types";
|
||||
import { AppState, BinaryFileData } from "../../packages/excalidraw/types";
|
||||
import Portal from "../collab/Portal";
|
||||
import type { SyncableExcalidrawElement } from ".";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import type { AppState, BinaryFileData } from "../../packages/excalidraw/types";
|
||||
import type Portal from "../collab/Portal";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
export interface StorageBackend {
|
||||
@ -10,7 +13,7 @@ export interface StorageBackend {
|
||||
portal: Portal,
|
||||
elements: readonly SyncableExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => Promise<false | { reconciledElements: any }>;
|
||||
) => Promise<false | readonly SyncableExcalidrawElement[] | null>;
|
||||
loadFromStorageBackend: (
|
||||
roomId: string,
|
||||
roomKey: string,
|
||||
|
@ -1,54 +1,54 @@
|
||||
import {
|
||||
isSavedToFirebase,
|
||||
loadFilesFromFirebase,
|
||||
loadFromFirebase,
|
||||
saveFilesToFirebase,
|
||||
saveToFirebase,
|
||||
} from "./firebase";
|
||||
import {
|
||||
isSavedToHttpStorage,
|
||||
loadFilesFromHttpStorage,
|
||||
loadFromHttpStorage,
|
||||
saveFilesToHttpStorage,
|
||||
saveToHttpStorage,
|
||||
} from "./httpStorage";
|
||||
import { StorageBackend } from "./StorageBackend";
|
||||
|
||||
const firebaseStorage: StorageBackend = {
|
||||
isSaved: isSavedToFirebase,
|
||||
saveToStorageBackend: saveToFirebase,
|
||||
loadFromStorageBackend: loadFromFirebase,
|
||||
saveFilesToStorageBackend: saveFilesToFirebase,
|
||||
loadFilesFromStorageBackend: loadFilesFromFirebase,
|
||||
};
|
||||
|
||||
const httpStorage: StorageBackend = {
|
||||
isSaved: isSavedToHttpStorage,
|
||||
saveToStorageBackend: saveToHttpStorage,
|
||||
loadFromStorageBackend: loadFromHttpStorage,
|
||||
saveFilesToStorageBackend: saveFilesToHttpStorage,
|
||||
loadFilesFromStorageBackend: loadFilesFromHttpStorage,
|
||||
};
|
||||
|
||||
const storageBackends = new Map<string, StorageBackend>()
|
||||
.set("firebase", firebaseStorage)
|
||||
.set("http", httpStorage);
|
||||
|
||||
export let storageBackend: StorageBackend | null = null;
|
||||
|
||||
export async function getStorageBackend() {
|
||||
if (storageBackend) {
|
||||
return storageBackend;
|
||||
}
|
||||
|
||||
const storageBackendName = import.meta.env.VITE_APP_STORAGE_BACKEND || '';
|
||||
|
||||
if (storageBackends.has(storageBackendName)) {
|
||||
storageBackend = storageBackends.get(storageBackendName) as StorageBackend;
|
||||
} else {
|
||||
console.warn("No storage backend found, default to firebase");
|
||||
storageBackend = firebaseStorage;
|
||||
}
|
||||
|
||||
isSavedToFirebase,
|
||||
loadFilesFromFirebase,
|
||||
loadFromFirebase,
|
||||
saveFilesToFirebase,
|
||||
saveToFirebase,
|
||||
} from "./firebase";
|
||||
import {
|
||||
isSavedToHttpStorage,
|
||||
loadFilesFromHttpStorage,
|
||||
loadFromHttpStorage,
|
||||
saveFilesToHttpStorage,
|
||||
saveToHttpStorage,
|
||||
} from "./httpStorage";
|
||||
import type { StorageBackend } from "./StorageBackend";
|
||||
|
||||
const firebaseStorage: StorageBackend = {
|
||||
isSaved: isSavedToFirebase,
|
||||
saveToStorageBackend: saveToFirebase,
|
||||
loadFromStorageBackend: loadFromFirebase,
|
||||
saveFilesToStorageBackend: saveFilesToFirebase,
|
||||
loadFilesFromStorageBackend: loadFilesFromFirebase,
|
||||
};
|
||||
|
||||
const httpStorage: StorageBackend = {
|
||||
isSaved: isSavedToHttpStorage,
|
||||
saveToStorageBackend: saveToHttpStorage,
|
||||
loadFromStorageBackend: loadFromHttpStorage,
|
||||
saveFilesToStorageBackend: saveFilesToHttpStorage,
|
||||
loadFilesFromStorageBackend: loadFilesFromHttpStorage,
|
||||
};
|
||||
|
||||
const storageBackends = new Map<string, StorageBackend>()
|
||||
.set("firebase", firebaseStorage)
|
||||
.set("http", httpStorage);
|
||||
|
||||
export let storageBackend: StorageBackend | null = null;
|
||||
|
||||
export async function getStorageBackend() {
|
||||
if (storageBackend) {
|
||||
return storageBackend;
|
||||
}
|
||||
}
|
||||
|
||||
const storageBackendName = import.meta.env.VITE_APP_STORAGE_BACKEND || "";
|
||||
|
||||
if (storageBackends.has(storageBackendName)) {
|
||||
storageBackend = storageBackends.get(storageBackendName) as StorageBackend;
|
||||
} else {
|
||||
console.warn("No storage backend found, default to firebase");
|
||||
storageBackend = firebaseStorage;
|
||||
}
|
||||
|
||||
return storageBackend;
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import {
|
||||
import { reconcileElements } from "../../packages/excalidraw";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import { getSceneVersion } from "../../packages/excalidraw/element";
|
||||
import Portal from "../collab/Portal";
|
||||
import type Portal from "../collab/Portal";
|
||||
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
||||
import {
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
@ -18,10 +20,11 @@ import {
|
||||
decryptData,
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
||||
import { reconcileElements } from "../collab/reconciliation";
|
||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||
import { ResolutionType } from "../../packages/excalidraw/utility-types";
|
||||
import type { SyncableExcalidrawElement } from ".";
|
||||
import { getSyncableElements } from ".";
|
||||
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
|
||||
|
||||
// private
|
||||
// -----------------------------------------------------------------------------
|
||||
@ -232,7 +235,7 @@ export const saveToFirebase = async (
|
||||
!socket ||
|
||||
isSavedToFirebase(portal, elements)
|
||||
) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const firebase = await loadFirestore();
|
||||
@ -240,56 +243,59 @@ export const saveToFirebase = async (
|
||||
|
||||
const docRef = firestore.collection("scenes").doc(roomId);
|
||||
|
||||
const savedData = await firestore.runTransaction(async (transaction) => {
|
||||
const storedScene = await firestore.runTransaction(async (transaction) => {
|
||||
const snapshot = await transaction.get(docRef);
|
||||
|
||||
if (!snapshot.exists) {
|
||||
const sceneDocument = await createFirebaseSceneDocument(
|
||||
const storedScene = await createFirebaseSceneDocument(
|
||||
firebase,
|
||||
elements,
|
||||
roomKey,
|
||||
);
|
||||
|
||||
transaction.set(docRef, sceneDocument);
|
||||
transaction.set(docRef, storedScene);
|
||||
|
||||
return {
|
||||
elements,
|
||||
reconciledElements: null,
|
||||
};
|
||||
return storedScene;
|
||||
}
|
||||
|
||||
const prevDocData = snapshot.data() as FirebaseStoredScene;
|
||||
const prevElements = getSyncableElements(
|
||||
await decryptElements(prevDocData, roomKey),
|
||||
const prevStoredScene = snapshot.data() as FirebaseStoredScene;
|
||||
const prevStoredElements = getSyncableElements(
|
||||
restoreElements(await decryptElements(prevStoredScene, roomKey), null),
|
||||
);
|
||||
|
||||
const reconciledElements = getSyncableElements(
|
||||
reconcileElements(elements, prevElements, appState),
|
||||
reconcileElements(
|
||||
elements,
|
||||
prevStoredElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[],
|
||||
appState,
|
||||
),
|
||||
);
|
||||
|
||||
const sceneDocument = await createFirebaseSceneDocument(
|
||||
const storedScene = await createFirebaseSceneDocument(
|
||||
firebase,
|
||||
reconciledElements,
|
||||
roomKey,
|
||||
);
|
||||
|
||||
transaction.update(docRef, sceneDocument);
|
||||
return {
|
||||
elements,
|
||||
reconciledElements,
|
||||
};
|
||||
transaction.update(docRef, storedScene);
|
||||
|
||||
// Return the stored elements as the in memory `reconciledElements` could have mutated in the meantime
|
||||
return storedScene;
|
||||
});
|
||||
|
||||
FirebaseSceneVersionCache.set(socket, savedData.elements);
|
||||
const storedElements = getSyncableElements(
|
||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||
);
|
||||
|
||||
return { reconciledElements: savedData.reconciledElements };
|
||||
FirebaseSceneVersionCache.set(socket, storedElements);
|
||||
|
||||
return storedElements;
|
||||
};
|
||||
|
||||
export const loadFromFirebase = async (
|
||||
roomId: string,
|
||||
roomKey: string,
|
||||
socket: Socket | null,
|
||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||
): Promise<readonly SyncableExcalidrawElement[] | null> => {
|
||||
const firebase = await loadFirestore();
|
||||
const db = firebase.firestore();
|
||||
|
||||
@ -300,14 +306,14 @@ export const loadFromFirebase = async (
|
||||
}
|
||||
const storedScene = doc.data() as FirebaseStoredScene;
|
||||
const elements = getSyncableElements(
|
||||
await decryptElements(storedScene, roomKey),
|
||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
||||
);
|
||||
|
||||
if (socket) {
|
||||
FirebaseSceneVersionCache.set(socket, elements);
|
||||
}
|
||||
|
||||
return restoreElements(elements, null);
|
||||
return elements;
|
||||
};
|
||||
|
||||
export const loadFilesFromFirebase = async (
|
||||
|
@ -1,34 +1,42 @@
|
||||
// Inspired and partly copied from https://gitlab.com/kiliandeca/excalidraw-fork
|
||||
// MIT, Kilian Decaderincourt
|
||||
|
||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||
import type { SyncableExcalidrawElement } from ".";
|
||||
import { getSyncableElements } from ".";
|
||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
||||
import { decompressData } from "../../packages/excalidraw/data/encode";
|
||||
import { encryptData, decryptData, IV_LENGTH_BYTES } from "../../packages/excalidraw/data/encryption";
|
||||
import {
|
||||
encryptData,
|
||||
decryptData,
|
||||
IV_LENGTH_BYTES,
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
||||
import { getSceneVersion } from "../../packages/excalidraw/element";
|
||||
import { ExcalidrawElement, FileId } from "../../packages/excalidraw/element/types";
|
||||
import {
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
DataURL,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import Portal from "../collab/Portal";
|
||||
import { reconcileElements } from "../collab/reconciliation";
|
||||
import { StoredScene } from "./StorageBackend";
|
||||
import type Portal from "../collab/Portal";
|
||||
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
|
||||
import { reconcileElements } from "../../packages/excalidraw/data/reconcile";
|
||||
import type { StoredScene } from "./StorageBackend";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const SCENE_VERSION_LENGTH_BYTES = 4
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env
|
||||
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const SCENE_VERSION_LENGTH_BYTES = 4;
|
||||
|
||||
// There is a lot of intentional duplication with the firebase file
|
||||
// to prevent modifying upstream files and ease futur maintenance of this fork
|
||||
|
||||
const httpStorageSceneVersionCache = new WeakMap<
|
||||
Socket,
|
||||
number
|
||||
>();
|
||||
const httpStorageSceneVersionCache = new WeakMap<Socket, number>();
|
||||
|
||||
export const isSavedToHttpStorage = (
|
||||
portal: Portal,
|
||||
@ -68,17 +76,20 @@ export const saveToHttpStorage = async (
|
||||
|
||||
if (!getResponse.ok && getResponse.status !== 404) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
if(getResponse.status === 404) {
|
||||
const result: boolean = await saveElementsToBackend(roomKey, roomId, [...elements], sceneVersion)
|
||||
if(result) {
|
||||
return {
|
||||
reconciledElements: null
|
||||
}
|
||||
if (getResponse.status === 404) {
|
||||
const result: boolean = await saveElementsToBackend(
|
||||
roomKey,
|
||||
roomId,
|
||||
[...elements],
|
||||
sceneVersion,
|
||||
);
|
||||
if (result) {
|
||||
return null;
|
||||
}
|
||||
return false
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
// If room already exist, we compare scene versions to check
|
||||
// if we're up to date before saving our scene
|
||||
@ -90,16 +101,23 @@ export const saveToHttpStorage = async (
|
||||
|
||||
const existingElements = await getElementsFromBuffer(buffer, roomKey);
|
||||
const reconciledElements = getSyncableElements(
|
||||
reconcileElements(elements, existingElements, appState),
|
||||
reconcileElements(
|
||||
elements,
|
||||
existingElements as OrderedExcalidrawElement[] as RemoteExcalidrawElement[],
|
||||
appState,
|
||||
),
|
||||
);
|
||||
|
||||
const result: boolean = await saveElementsToBackend(roomKey, roomId, reconciledElements, sceneVersion)
|
||||
const result: boolean = await saveElementsToBackend(
|
||||
roomKey,
|
||||
roomId,
|
||||
reconciledElements,
|
||||
sceneVersion,
|
||||
);
|
||||
|
||||
if (result) {
|
||||
httpStorageSceneVersionCache.set(socket, sceneVersion);
|
||||
return {
|
||||
reconciledElements: elements
|
||||
};
|
||||
return elements;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@ -109,7 +127,8 @@ export const loadFromHttpStorage = async (
|
||||
roomKey: string,
|
||||
socket: Socket | null,
|
||||
): Promise<readonly ExcalidrawElement[] | null> => {
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env
|
||||
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const getResponse = await fetch(
|
||||
`${HTTP_STORAGE_BACKEND_URL}/rooms/${roomId}`,
|
||||
);
|
||||
@ -130,12 +149,20 @@ const getElementsFromBuffer = async (
|
||||
): Promise<readonly ExcalidrawElement[]> => {
|
||||
// Buffer should contain both the IV (fixed length) and encrypted data
|
||||
const sceneVersion = parseSceneVersionFromRequest(buffer);
|
||||
const iv = new Uint8Array(buffer.slice(SCENE_VERSION_LENGTH_BYTES, IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES));
|
||||
const encrypted = buffer.slice(IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES, buffer.byteLength);
|
||||
const iv = new Uint8Array(
|
||||
buffer.slice(
|
||||
SCENE_VERSION_LENGTH_BYTES,
|
||||
IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES,
|
||||
),
|
||||
);
|
||||
const encrypted = buffer.slice(
|
||||
IV_LENGTH_BYTES + SCENE_VERSION_LENGTH_BYTES,
|
||||
buffer.byteLength,
|
||||
);
|
||||
|
||||
return await decryptElements(
|
||||
{ sceneVersion: sceneVersion, ciphertext: encrypted, iv },
|
||||
key
|
||||
{ sceneVersion, ciphertext: encrypted, iv },
|
||||
key,
|
||||
);
|
||||
};
|
||||
|
||||
@ -149,7 +176,8 @@ export const saveFilesToHttpStorage = async ({
|
||||
const erroredFiles = new Map<FileId, true>();
|
||||
const savedFiles = new Map<FileId, true>();
|
||||
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env
|
||||
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
|
||||
await Promise.all(
|
||||
files.map(async ({ id, buffer }) => {
|
||||
@ -182,7 +210,8 @@ export const loadFilesFromHttpStorage = async (
|
||||
await Promise.all(
|
||||
[...new Set(filesIds)].map(async (id) => {
|
||||
try {
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const HTTP_STORAGE_BACKEND_URL = import.meta.env
|
||||
.VITE_APP_HTTP_STORAGE_BACKEND_URL;
|
||||
const response = await fetch(`${HTTP_STORAGE_BACKEND_URL}/files/${id}`);
|
||||
if (response.status < 400) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
@ -216,7 +245,12 @@ export const loadFilesFromHttpStorage = async (
|
||||
return { loadedFiles, erroredFiles };
|
||||
};
|
||||
|
||||
const saveElementsToBackend = async (roomKey: string, roomId: string, elements: SyncableExcalidrawElement[], sceneVersion: number) => {
|
||||
const saveElementsToBackend = async (
|
||||
roomKey: string,
|
||||
roomId: string,
|
||||
elements: SyncableExcalidrawElement[],
|
||||
sceneVersion: number,
|
||||
) => {
|
||||
const { ciphertext, iv } = await encryptElements(roomKey, elements);
|
||||
|
||||
// Concatenate Scene Version, IV with encrypted data (IV does not have to be secret).
|
||||
@ -224,7 +258,9 @@ const saveElementsToBackend = async (roomKey: string, roomId: string, elements:
|
||||
const numberView = new DataView(numberBuffer);
|
||||
numberView.setUint32(0, sceneVersion, false);
|
||||
const sceneVersionBuffer = numberView.buffer;
|
||||
const payloadBlob = await new Response(new Blob([sceneVersionBuffer, iv.buffer, ciphertext])).arrayBuffer();
|
||||
const payloadBlob = await new Response(
|
||||
new Blob([sceneVersionBuffer, iv.buffer, ciphertext]),
|
||||
).arrayBuffer();
|
||||
const putResponse = await fetch(
|
||||
`${HTTP_STORAGE_BACKEND_URL}/rooms/${roomId}`,
|
||||
{
|
||||
@ -233,13 +269,13 @@ const saveElementsToBackend = async (roomKey: string, roomId: string, elements:
|
||||
},
|
||||
);
|
||||
|
||||
return putResponse.ok
|
||||
}
|
||||
return putResponse.ok;
|
||||
};
|
||||
|
||||
const parseSceneVersionFromRequest = (buffer: ArrayBuffer) => {
|
||||
const view = new DataView(buffer);
|
||||
return view.getUint32(0, false);
|
||||
}
|
||||
};
|
||||
|
||||
const decryptElements = async (
|
||||
data: StoredScene,
|
||||
@ -264,4 +300,4 @@ const encryptElements = async (
|
||||
const { encryptedBuffer, iv } = await encryptData(key, encoded);
|
||||
|
||||
return { ciphertext: encryptedBuffer, iv };
|
||||
};
|
||||
};
|
||||
|
@ -4,44 +4,44 @@ import {
|
||||
} from "../../packages/excalidraw/data/encode";
|
||||
import {
|
||||
decryptData,
|
||||
encryptData,
|
||||
generateEncryptionKey,
|
||||
IV_LENGTH_BYTES,
|
||||
} from "../../packages/excalidraw/data/encryption";
|
||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||
import { restore } from "../../packages/excalidraw/data/restore";
|
||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||
import {
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "../../packages/excalidraw/element/types";
|
||||
import { t } from "../../packages/excalidraw/i18n";
|
||||
import {
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
SocketId,
|
||||
UserIdleState,
|
||||
} from "../../packages/excalidraw/types";
|
||||
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
|
||||
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
||||
import type { WS_SUBTYPES } from "../app_constants";
|
||||
import {
|
||||
DELETED_ELEMENT_TIMEOUT,
|
||||
FILE_UPLOAD_MAX_BYTES,
|
||||
ROOM_ID_BYTES,
|
||||
WS_SUBTYPES,
|
||||
} from "../app_constants";
|
||||
import { encodeFilesForUpload } from "./FileManager";
|
||||
import { getStorageBackend } from "./config";
|
||||
|
||||
export type SyncableExcalidrawElement = ExcalidrawElement & {
|
||||
_brand: "SyncableExcalidrawElement";
|
||||
};
|
||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||
MakeBrand<"SyncableExcalidrawElement">;
|
||||
|
||||
export const isSyncableElement = (
|
||||
element: ExcalidrawElement,
|
||||
element: OrderedExcalidrawElement,
|
||||
): element is SyncableExcalidrawElement => {
|
||||
if (element.isDeleted) {
|
||||
if (element.updated > Date.now() - DELETED_ELEMENT_TIMEOUT) {
|
||||
@ -52,7 +52,9 @@ export const isSyncableElement = (
|
||||
return !isInvisiblySmallElement(element);
|
||||
};
|
||||
|
||||
export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
|
||||
export const getSyncableElements = (
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
) =>
|
||||
elements.filter((element) =>
|
||||
isSyncableElement(element),
|
||||
) as SyncableExcalidrawElement[];
|
||||
@ -66,35 +68,6 @@ const generateRoomId = async () => {
|
||||
return bytesToHexString(buffer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Right now the reason why we resolve connection params (url, polling...)
|
||||
* from upstream is to allow changing the params immediately when needed without
|
||||
* having to wait for clients to update the SW.
|
||||
*
|
||||
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
|
||||
*/
|
||||
export const getCollabServer = async (): Promise<{
|
||||
url: string;
|
||||
polling: boolean;
|
||||
}> => {
|
||||
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
|
||||
return {
|
||||
url: import.meta.env.VITE_APP_WS_SERVER_URL,
|
||||
polling: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
|
||||
);
|
||||
return await resp.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(t("errors.cannotResolveCollabServer"));
|
||||
}
|
||||
};
|
||||
|
||||
export type EncryptedData = {
|
||||
data: ArrayBuffer;
|
||||
iv: Uint8Array;
|
||||
@ -296,7 +269,6 @@ export const loadScene = async (
|
||||
// in the scene database/localStorage, and instead fetch them async
|
||||
// from a different database
|
||||
files: data.files,
|
||||
commitToHistory: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||
import { AppState } from "../../packages/excalidraw/types";
|
||||
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||
import type { AppState } from "../../packages/excalidraw/types";
|
||||
import {
|
||||
clearAppStateForLocalStorage,
|
||||
getDefaultAppState,
|
||||
} from "../../packages/excalidraw/appState";
|
||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||
|
||||
export const saveUsernameToLocalStorage = (username: string) => {
|
||||
try {
|
||||
@ -88,28 +87,13 @@ export const getTotalStorageSize = () => {
|
||||
try {
|
||||
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
||||
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||
const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
|
||||
|
||||
const appStateSize = appState?.length || 0;
|
||||
const collabSize = collab?.length || 0;
|
||||
const librarySize = library?.length || 0;
|
||||
|
||||
return appStateSize + collabSize + librarySize + getElementsStorageSize();
|
||||
return appStateSize + collabSize + getElementsStorageSize();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLibraryItemsFromStorage = () => {
|
||||
try {
|
||||
const libraryItems: ImportedDataState["libraryItems"] = JSON.parse(
|
||||
localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string,
|
||||
);
|
||||
|
||||
return libraryItems || [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user