Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"dependencies": {
"@arco-design/web-react": "^2.66.7",
"@aws-sdk/client-s3": "^3.981.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
Expand Down
19 changes: 18 additions & 1 deletion packages/filesystem/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import OneDriveFileSystem from "./onedrive/onedrive";
import DropboxFileSystem from "./dropbox/dropbox";
import WebDAVFileSystem from "./webdav/webdav";
import ZipFileSystem from "./zip/zip";
import S3FileSystem from "./s3/s3";
import { t } from "@App/locales/locales";
import LimiterFileSystem from "./limiter";

export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive" | "googledrive" | "dropbox";
export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive" | "googledrive" | "dropbox" | "s3";

export type FileSystemParams = {
[key: string]: {
Expand Down Expand Up @@ -40,6 +41,15 @@ export default class FileSystemFactory {
case "dropbox":
fs = new DropboxFileSystem();
break;
case "s3":
fs = new S3FileSystem(
params.bucket,
params.region,
params.accessKeyId,
params.secretAccessKey,
params.endpoint
);
break;
default:
throw new Error("not found filesystem");
}
Expand All @@ -63,6 +73,13 @@ export default class FileSystemFactory {
onedrive: {},
googledrive: {},
dropbox: {},
s3: {
bucket: { title: t("s3_bucket_name") },
region: { title: t("s3_region") },
accessKeyId: { title: t("s3_access_key_id") },
secretAccessKey: { title: t("s3_secret_access_key"), type: "password" },
endpoint: { title: t("s3_custom_endpoint") },
},
};
}

Expand Down
111 changes: 111 additions & 0 deletions packages/filesystem/s3/rw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import type { S3Client } from "@aws-sdk/client-s3";
import type { FileReader, FileWriter } from "../filesystem";

/**
* FileReader implementation for Amazon S3.
* Downloads and reads file content from S3.
*/
export class S3FileReader implements FileReader {
client: S3Client;

bucket: string;

key: string;

constructor(client: S3Client, bucket: string, key: string) {
this.client = client;
this.bucket = bucket;
this.key = key;
}

/**
* Reads file content from S3.
* @param type - Output format: "string" for text, "blob" for binary (default)
* @returns File content as string or Blob
* @throws {Error} If file not found or read fails
*/
async read(type: "string" | "blob" = "blob"): Promise<string | Blob> {
try {
const command = new GetObjectCommand({
Bucket: this.bucket,
Key: this.key,
});

const response = await this.client.send(command);

if (!response.Body) {
throw new Error("Empty response body from S3");
}

// Convert the stream to the requested format
const stream = response.Body.transformToWebStream();

if (type === "string") {
// Streaming decode to string (省内存)
const reader = stream.getReader();
const decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
}
// 最后 flush
result += decoder.decode();
return result;
} else {
// 返回 Blob,避免 JS 层缓冲与拼接,走底层最优路径
return new Response(stream).blob();
}
} catch (error: any) {
if (error.name === "NoSuchKey") {
throw new Error(`File not found: ${this.key}`);
}
throw error;
}
}
}

/**
* FileWriter implementation for Amazon S3.
* Uploads file content to S3 with optional metadata.
*/
export class S3FileWriter implements FileWriter {
client: S3Client;

bucket: string;

key: string;

modifiedDate?: number;

constructor(client: S3Client, bucket: string, key: string, modifiedDate?: number) {
this.client = client;
this.bucket = bucket;
this.key = key;
this.modifiedDate = modifiedDate;
}

/**
* Writes content to S3.
* @param content - File content as string or Blob
* @throws {Error} If upload fails
*/
async write(content: string | Blob): Promise<void> {
const metadata: Record<string, string> = {};
if (this.modifiedDate) {
// 用 ISO 8601 格式
metadata.createtime = new Date(this.modifiedDate).toISOString(); // 规范格式
}

const command = new PutObjectCommand({
Bucket: this.bucket,
Key: this.key,
Body: content, // API 的 Body 接受 string | Blob | Uint8Array<ArrayBufferLike> | Buffer<ArrayBufferLike> | Readable | ReadableStream<any>
Metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
});

await this.client.send(command);
}
}
Loading
Loading