diff --git a/src/static/aws/s3.service.spec.ts b/src/static/aws/s3.service.spec.ts index af53cf65..d7bdc4df 100644 --- a/src/static/aws/s3.service.spec.ts +++ b/src/static/aws/s3.service.spec.ts @@ -8,8 +8,9 @@ import { generateNewImageName } from '../utils'; const mockSend = jest.fn(); jest.mock('@aws-sdk/client-s3', () => ({ - S3Client: jest.fn().mockImplementation(() => ({ + S3Client: jest.fn().mockImplementation((config) => ({ send: mockSend, + config, })), PutObjectCommand: jest.fn().mockImplementation((input) => ({ input, type: 'put' })), GetObjectCommand: jest.fn().mockImplementation((input) => ({ input, type: 'get' })), @@ -232,4 +233,34 @@ describe('AWSS3Service', () => { expect(loggerSpy).toHaveBeenCalledWith('Failed to delete file at AWS S3 for image image.png:', error); }); }); + + // S3-compatible storage — MinIO, Ceph, Garage — is normally reached + // path-style (host/bucket/key); AWS itself uses virtual-host style + // (bucket.host/key) and the SDK assumes that. Without a way to switch, a + // self-hosted deployment cannot point VRT at its own storage, and neither + // can anyone trying to reproduce a production setup locally. + describe('S3-compatible storage', () => { + const originalPathStyle = process.env.AWS_S3_FORCE_PATH_STYLE; + + afterEach(() => { + process.env.AWS_S3_FORCE_PATH_STYLE = originalPathStyle; + if (originalPathStyle === undefined) delete process.env.AWS_S3_FORCE_PATH_STYLE; + }); + + it('addresses the bucket path-style when asked to', () => { + process.env.AWS_S3_FORCE_PATH_STYLE = 'true'; + + const service = new AWSS3Service(); + + expect((service as any).s3Client.config).toMatchObject({ forcePathStyle: true }); + }); + + it('leaves the SDK to its own addressing by default', () => { + delete process.env.AWS_S3_FORCE_PATH_STYLE; + + const service = new AWSS3Service(); + + expect((service as any).s3Client.config?.forcePathStyle).toBeFalsy(); + }); + }); }); diff --git a/src/static/aws/s3.service.ts b/src/static/aws/s3.service.ts index db6fed5a..f622ce92 100644 --- a/src/static/aws/s3.service.ts +++ b/src/static/aws/s3.service.ts @@ -23,8 +23,18 @@ export class AWSS3Service implements Static { private s3Client: S3Client; constructor() { - this.s3Client = new S3Client(); - this.logger.log('AWS S3 service is being used for file storage.'); + // S3-compatible storage — MinIO, Ceph, Garage — is normally addressed + // path-style (host/bucket/key), while AWS uses virtual-host style + // (bucket.host/key) and the SDK assumes that. It has no environment + // variable for the switch, so without this a self-hosted deployment cannot + // point VRT at its own storage, and neither can anyone standing up a + // production-shaped stack locally. The endpoint itself the SDK does read + // from AWS_ENDPOINT_URL. + const forcePathStyle = process.env.AWS_S3_FORCE_PATH_STYLE === 'true'; + this.s3Client = new S3Client(forcePathStyle ? { forcePathStyle } : {}); + this.logger.log( + `AWS S3 service is being used for file storage${forcePathStyle ? ' (path-style addressing)' : ''}.` + ); } async saveImage(type: 'screenshot' | 'diff' | 'baseline', imageBuffer: Buffer): Promise {