fix(lms): support direct R2 map uploads and preserve empty course groups

This commit is contained in:
2026-09-09 13:57:28 +09:00
parent 121bf41266
commit d70f99e9c9
12 changed files with 348 additions and 57 deletions
@@ -0,0 +1,18 @@
import { ConfigService } from '@nestjs/config';
import { R2PublicFileService } from './r2-public-file.service';
describe('R2 direct upload URL', () => {
it('signs a PUT without a checksum for an empty body', async () => {
const service = new R2PublicFileService(new ConfigService({
R2_BUCKET: 'test', R2_ACCOUNT_ID: 'test',
R2_PUBLIC_BASE_URL: 'https://files.example.com',
R2_ACCESS_KEY_ID: 'test', R2_SECRET_ACCESS_KEY: 'test',
}));
const url = new URL((await service.createUploadUrl('studio-files/public/test.db'))!);
expect(url.protocol).toBe('https:');
expect(url.searchParams.get('X-Amz-Expires')).toBe('3600');
expect(url.searchParams.has('X-Amz-Signature')).toBe(true);
expect(url.searchParams.has('x-amz-checksum-crc32')).toBe(false);
expect(await service.createUploadUrl('private/test.db')).toBeNull();
});
});
+19 -1
View File
@@ -1,4 +1,5 @@
import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { DeleteObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@@ -86,6 +87,21 @@ export class R2PublicFileService {
}));
}
async createUploadUrl(key: string) {
if (!this.isConfigured() || !this.isPublicKey(key)) return null;
return getSignedUrl(this.getClient(), new PutObjectCommand({
Bucket: this.bucket,
Key: key,
ContentType: 'application/octet-stream',
}), { expiresIn: 3600 });
}
async verifyUploadedFile(key: string) {
if (!this.isConfigured() || !this.isPublicKey(key)) return null;
const result = await this.getClient().send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
return (result.ContentLength ?? 0) > 0 ? this.publicUrl(key) : null;
}
publicUrl(key: string) {
if (!this.publicBaseUrl || !this.isPublicKey(key)) return null;
return `${this.publicBaseUrl.replace(/\/$/, '')}/${encodeObjectKey(key)}`;
@@ -109,6 +125,8 @@ export class R2PublicFileService {
secretAccessKey: this.secretAccessKey,
},
forcePathStyle: this.configService.get<string>('R2_FORCE_PATH_STYLE', 'true') !== 'false',
// Presigned PUT bodies are supplied later by the platform, not this client.
requestChecksumCalculation: 'WHEN_REQUIRED',
});
return this.client;