UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

259 lines 10.2 kB
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