feat(server): lightweight s3 client (#14348)

#### PR Dependency Tree


* **PR #14348** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a dedicated S3-compatible client package and expanded
S3-compatible storage config (endpoint, region, forcePathStyle,
requestTimeoutMs, minPartSize, presign options, sessionToken).
* Document sync now broadcasts batched/compressed doc updates for more
efficient real-time syncing.

* **Tests**
* New unit and benchmark tests for base64 utilities and S3 multipart
listing; updated storage-related tests to match new formats.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-01 21:54:39 +08:00
committed by GitHub
parent 059d3aa04a
commit f1a6e409cb
37 changed files with 1539 additions and 1712 deletions

View File

@@ -1,8 +1,7 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { PutObjectCommandInput } from '@aws-sdk/client-s3';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { createS3CompatClient } from '@affine/s3-compat';
import { lookup } from 'mime-types';
import type { Compiler, WebpackPluginInstance } from 'webpack';
@@ -11,16 +10,18 @@ export const R2_BUCKET =
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
export class WebpackS3Plugin implements WebpackPluginInstance {
private readonly s3 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
private readonly s3 = createS3CompatClient(
{
region: 'auto',
bucket: R2_BUCKET,
forcePathStyle: true,
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
},
{
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
requestChecksumCalculation: 'WHEN_REQUIRED',
responseChecksumValidation: 'WHEN_REQUIRED',
});
}
);
apply(compiler: Compiler) {
compiler.hooks.assetEmitted.tapPromise(
@@ -31,16 +32,11 @@ export class WebpackS3Plugin implements WebpackPluginInstance {
}
const assetPath = join(outputPath, asset);
const assetSource = await readFile(assetPath);
const putObjectCommandOptions: PutObjectCommandInput = {
Body: assetSource,
Bucket: R2_BUCKET,
Key: asset,
};
const contentType = lookup(asset);
if (contentType) {
putObjectCommandOptions.ContentType = contentType;
}
await this.s3.send(new PutObjectCommand(putObjectCommandOptions));
const contentType = lookup(asset) || undefined;
await this.s3.putObject(asset, assetSource, {
contentType,
contentLength: assetSource.byteLength,
});
}
);
}