Files
Excalidraw/excalidraw/excalidraw-app/data/storageAdapters/S3StorageAdapter.ts
T
Yuzhong Zhang 602f4629ff init frontend
2025-07-05 23:22:48 +08:00

198 lines
5.3 KiB
TypeScript

import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
PutObjectCommand,
DeleteObjectsCommand,
} from "@aws-sdk/client-s3";
import {
CanvasData,
CanvasMetadata,
dehydrateCanvasData,
hydrateCanvasData,
IStorageAdapter,
} from "../storage";
import { generateThumbnail } from "../thumbnail";
const KEY_PREFIX_METADATA = "excalidraw-canvas-meta-";
const KEY_PREFIX_DATA = "excalidraw-canvas-data-";
export class S3StorageAdapter implements IStorageAdapter {
private s3: S3Client;
private bucketName: string;
constructor(config: {
accessKeyId: string;
secretAccessKey: string;
region: string;
bucketName: string;
}) {
this.s3 = new S3Client({
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
});
this.bucketName = config.bucketName;
}
private async getObject(key: string): Promise<any | null> {
try {
const command = new GetObjectCommand({
Bucket: this.bucketName,
Key: key,
});
const response = await this.s3.send(command);
const body = await response.Body?.transformToString();
return body ? JSON.parse(body) : null;
} catch (error: any) {
if (error.name === "NoSuchKey") {
return null;
}
throw error;
}
}
async listCanvases(): Promise<CanvasMetadata[]> {
const command = new ListObjectsV2Command({
Bucket: this.bucketName,
Prefix: KEY_PREFIX_METADATA,
});
const response = await this.s3.send(command);
if (!response.Contents) {
return [];
}
const metadataPromises = response.Contents.map((obj: { Key?: string }) =>
this.getObject(obj.Key!),
);
const results = await Promise.all(metadataPromises);
return results.filter(
(m: CanvasMetadata | null): m is CanvasMetadata => m !== null,
);
}
async loadCanvas(id: string): Promise<CanvasData | null> {
const rawData = await this.getObject(`${KEY_PREFIX_DATA}${id}`);
return rawData ? hydrateCanvasData(rawData) : null;
}
async saveCanvas(id: string, data: CanvasData): Promise<void> {
const metadataKey = `${KEY_PREFIX_METADATA}${id}`;
const dataKey = `${KEY_PREFIX_DATA}${id}`;
const existingMetadata = await this.getObject(metadataKey);
if (!existingMetadata) {
throw new Error("Canvas metadata not found. Cannot save.");
}
const thumbnail = await generateThumbnail(
data.elements,
data.appState,
data.files,
);
const updatedMetadata: CanvasMetadata = {
...existingMetadata,
name: data.appState.name || existingMetadata.name,
updatedAt: new Date().toISOString(),
thumbnail: data.elements.length > 0 ? thumbnail : undefined,
};
const dehydratedData = dehydrateCanvasData(data);
await Promise.all([
this.s3.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: metadataKey,
Body: JSON.stringify(updatedMetadata),
ContentType: "application/json",
}),
),
this.s3.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: dataKey,
Body: JSON.stringify(dehydratedData),
ContentType: "application/json",
}),
),
]);
}
async createCanvas(data: CanvasData): Promise<CanvasMetadata> {
const newId = window.crypto.randomUUID();
const now = new Date().toISOString();
const thumbnail = await generateThumbnail(
data.elements,
data.appState,
data.files,
);
const newMetadata: CanvasMetadata = {
id: newId,
name: data.appState.name || "Untitled Canvas",
createdAt: now,
updatedAt: now,
userId: 0,
thumbnail: data.elements.length > 0 ? thumbnail : undefined,
};
const metadataKey = `${KEY_PREFIX_METADATA}${newId}`;
const dataKey = `${KEY_PREFIX_DATA}${newId}`;
const dehydratedData = dehydrateCanvasData(data);
await Promise.all([
this.s3.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: metadataKey,
Body: JSON.stringify(newMetadata),
ContentType: "application/json",
}),
),
this.s3.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: dataKey,
Body: JSON.stringify(dehydratedData),
ContentType: "application/json",
}),
),
]);
return newMetadata;
}
async deleteCanvas(id: string): Promise<void> {
const command = new DeleteObjectsCommand({
Bucket: this.bucketName,
Delete: {
Objects: [
{ Key: `${KEY_PREFIX_METADATA}${id}` },
{ Key: `${KEY_PREFIX_DATA}${id}` },
],
},
});
await this.s3.send(command);
}
async renameCanvas(id: string, newName: string): Promise<void> {
const metadataKey = `${KEY_PREFIX_METADATA}${id}`;
const metadata = await this.getObject(metadataKey);
if (!metadata) {
throw new Error("Canvas metadata not found. Cannot rename.");
}
metadata.name = newName;
metadata.updatedAt = new Date().toISOString();
const data = await this.loadCanvas(id);
if (!data) {
throw new Error("Canvas data not found. Cannot rename.");
}
data.appState.name = newName;
await this.saveCanvas(id, data);
}
}