safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
259 lines • 10.2 kB
JavaScript
import { S3UploadError } from '../../core/errors.js';
import { sanitizeFilename } from '../../utils/guards.js';
import { noOpLogger } from '../../config/defaults.js';
/**
* Default S3 uploader implementation
*/
export class DefaultS3Uploader {
constructor(config, logger = noOpLogger) {
this.config = config;
this.logger = logger;
}
/**
* Initialize S3 client (lazy loading to avoid requiring AWS SDK)
*/
async initS3Client() {
if (this.s3Client) {
return this.s3Client;
}
try {
// Dynamic import to avoid bundling AWS SDK if not used
const { S3Client } = await import('@aws-sdk/client-s3');
this.s3Client = new S3Client({
region: this.config.region,
credentials: this.config.accessKeyId && this.config.secretAccessKey
? {
accessKeyId: this.config.accessKeyId,
secretAccessKey: this.config.secretAccessKey,
}
: undefined, // Use default credential provider chain if not provided
endpoint: this.config.endpoint,
});
this.logger.debug('S3 client initialized successfully');
return this.s3Client;
}
catch (error) {
throw new S3UploadError('Failed to initialize S3 client. Make sure @aws-sdk/client-s3 is installed.', this.config.bucket, undefined, error);
}
}
/**
* Upload buffer to S3
*/
async upload(buffer, filename, mimeType = 'application/pdf', options = {}) {
const s3Client = await this.initS3Client();
const sanitizedFilename = sanitizeFilename(filename);
const folder = options.folder || this.config.folder || 'pdf-reports';
const key = folder ? `${folder}/${sanitizedFilename}` : sanitizedFilename;
const acl = options.acl || this.config.acl || 'public-read';
this.logger.info(`Uploading to S3: s3://${this.config.bucket}/${key}`);
try {
// Dynamic import of PutObjectCommand
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
const command = new PutObjectCommand({
Bucket: this.config.bucket,
Key: key,
Body: buffer,
ContentType: mimeType,
ContentDisposition: 'inline',
ACL: acl, // Type assertion for S3 ACL
});
const result = await s3Client.send(command);
const url = this.config.endpoint
? `${this.config.endpoint}/${this.config.bucket}/${key}`
: `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${key}`;
this.logger.info(`Upload successful: ${key} (${(buffer.length / 1024 / 1024).toFixed(2)}MB, ETag: ${result.ETag})`);
return { url, key };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`S3 upload failed: ${errorMessage}`);
throw new S3UploadError(`Failed to upload to S3: ${errorMessage}`, this.config.bucket, key, error);
}
}
/**
* Check if object exists in S3
*/
async exists(key) {
const s3Client = await this.initS3Client();
try {
const { HeadObjectCommand } = await import('@aws-sdk/client-s3');
const command = new HeadObjectCommand({
Bucket: this.config.bucket,
Key: key,
});
await s3Client.send(command);
return true;
}
catch (error) {
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
return false;
}
throw new S3UploadError(`Failed to check object existence: ${error.message}`, this.config.bucket, key, error);
}
}
/**
* Delete object from S3
*/
async delete(key) {
const s3Client = await this.initS3Client();
try {
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
const command = new DeleteObjectCommand({
Bucket: this.config.bucket,
Key: key,
});
await s3Client.send(command);
this.logger.info(`Deleted from S3: ${key}`);
}
catch (error) {
throw new S3UploadError(`Failed to delete from S3: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.bucket, key, error);
}
}
/**
* Get object metadata
*/
async getMetadata(key) {
const s3Client = await this.initS3Client();
try {
const { HeadObjectCommand } = await import('@aws-sdk/client-s3');
const command = new HeadObjectCommand({
Bucket: this.config.bucket,
Key: key,
});
const result = await s3Client.send(command);
return {
size: result.ContentLength || 0,
lastModified: result.LastModified || new Date(),
contentType: result.ContentType,
etag: result.ETag,
};
}
catch (error) {
throw new S3UploadError(`Failed to get metadata: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.bucket, key, error);
}
}
/**
* Generate presigned URL for temporary access
*/
async generatePresignedUrl(key, expiresInSeconds = 3600) {
const s3Client = await this.initS3Client();
try {
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
// Try to import the presigner, fallback if not available
let getSignedUrl;
try {
// Use eval to bypass TypeScript module resolution
const presignerModuleName = '@aws-sdk/s3-request-presigner';
const presigner = await eval(`import('${presignerModuleName}')`);
getSignedUrl = presigner.getSignedUrl;
}
catch (importError) {
throw new Error('The @aws-sdk/s3-request-presigner package is required for generating presigned URLs. ' +
'Please install it with: npm install @aws-sdk/s3-request-presigner');
}
const command = new GetObjectCommand({
Bucket: this.config.bucket,
Key: key,
});
const url = await getSignedUrl(s3Client, command, {
expiresIn: expiresInSeconds,
});
return url;
}
catch (error) {
throw new S3UploadError(`Failed to generate presigned URL: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.bucket, key, error);
}
}
/**
* List objects with prefix
*/
async listObjects(prefix, maxKeys = 1000) {
const s3Client = await this.initS3Client();
try {
const { ListObjectsV2Command } = await import('@aws-sdk/client-s3');
const command = new ListObjectsV2Command({
Bucket: this.config.bucket,
Prefix: prefix,
MaxKeys: maxKeys,
});
const result = await s3Client.send(command);
return (result.Contents || []).map((obj) => ({
key: obj.Key || '',
size: obj.Size || 0,
lastModified: obj.LastModified || new Date(),
etag: obj.ETag || '',
}));
}
catch (error) {
throw new S3UploadError(`Failed to list objects: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.bucket, undefined, error);
}
}
/**
* Get bucket region
*/
async getBucketRegion() {
const s3Client = await this.initS3Client();
try {
const { GetBucketLocationCommand } = await import('@aws-sdk/client-s3');
const command = new GetBucketLocationCommand({
Bucket: this.config.bucket,
});
const result = await s3Client.send(command);
return result.LocationConstraint || 'us-east-1';
}
catch (error) {
throw new S3UploadError(`Failed to get bucket region: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.bucket, undefined, error);
}
}
/**
* Test S3 connectivity and permissions
*/
async testConnection() {
try {
// Test basic connectivity by getting bucket location
await this.getBucketRegion();
// Test write permissions with a small test file
const testKey = `test-${Date.now()}.txt`;
const testBuffer = Buffer.from('test');
let canWrite = false;
try {
await this.upload(testBuffer, testKey, 'text/plain');
canWrite = true;
// Clean up test file
await this.delete(testKey);
}
catch (writeError) {
this.logger.warn(`Write test failed: ${writeError}`);
}
// Test read permissions by listing objects
let canRead = false;
try {
await this.listObjects('', 1);
canRead = true;
}
catch (readError) {
this.logger.warn(`Read test failed: ${readError}`);
}
return {
success: true,
canRead,
canWrite,
};
}
catch (error) {
return {
success: false,
canRead: false,
canWrite: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
}
/**
* Create S3 uploader from configuration
*/
export function createS3Uploader(config, logger) {
return new DefaultS3Uploader(config, logger);
}
//# sourceMappingURL=S3Uploader.js.map