@prodbirdy/mockup-generator
Version:
Serverless-optimized TypeScript SDK for generating high-quality product mockups from PSD templates
259 lines (228 loc) • 6.62 kB
text/typescript
/**
* Storage Manager for MockupSDK
* Handles R2/S3 operations with better error handling and URL-based operations
*/
import { EventEmitter } from "events";
import { R2Client, generateUniqueFileName } from "../utils/r2-helpers";
import type { StorageConfig, ExportFormat } from "./types";
import { StorageError } from "./types";
export class StorageManager extends EventEmitter {
private client: R2Client | null = null;
private config: StorageConfig | null = null;
constructor(config?: StorageConfig) {
super();
if (config) {
this.config = config;
this.client = new R2Client(config);
} else {
// Try to load from environment variables
this.loadFromEnvironment();
}
}
/**
* Upload a PSD buffer to storage
*/
async uploadPSD(psdBuffer: Buffer, prefix = "mockups"): Promise<string> {
if (!this.client) {
throw new StorageError("Storage not configured");
}
try {
this.emit("progress", "uploading-psd", 0);
const fileName = generateUniqueFileName("mockup", "psd");
const result = await this.client.uploadPSD(
psdBuffer,
fileName,
prefix,
false
);
this.emit("progress", "uploading-psd", 100);
return result.url;
} catch (error) {
throw new StorageError(
`Failed to upload PSD: ${
error instanceof Error ? error.message : String(error)
}`,
error instanceof Error ? error : undefined
);
}
}
/**
* Upload an exported image to storage
*/
async uploadExport(
buffer: Buffer,
format: ExportFormat,
prefix = "exports"
): Promise<{ url: string; key: string; bucket: string }> {
if (!this.client) {
throw new StorageError("Storage not configured");
}
try {
const fileName = generateUniqueFileName("export", format);
const result = await this.client.uploadImage(
buffer,
fileName,
format as any,
prefix,
false
);
return {
url: result.url,
key: result.key,
bucket: result.bucket,
};
} catch (error) {
throw new StorageError(
`Failed to upload ${format}: ${
error instanceof Error ? error.message : String(error)
}`,
error instanceof Error ? error : undefined
);
}
}
/**
* Download a PSD from storage
*/
async downloadPSD(url: string): Promise<Buffer> {
try {
this.emit("progress", "downloading-psd", 0);
// If it's a URL, fetch directly
if (url.startsWith("http")) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
this.emit("progress", "downloading-psd", 100);
return buffer;
}
// If it's a key, use the R2 client
if (!this.client) {
throw new StorageError(
"Storage not configured for key-based downloads"
);
}
const buffer = await this.client.download(url, false);
this.emit("progress", "downloading-psd", 100);
return buffer;
} catch (error) {
throw new StorageError(
`Failed to download PSD: ${
error instanceof Error ? error.message : String(error)
}`,
error instanceof Error ? error : undefined
);
}
}
/**
* Download an image for smart object replacement
*/
async downloadImage(url: string): Promise<Buffer> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
} catch (error) {
throw new StorageError(
`Failed to download image: ${
error instanceof Error ? error.message : String(error)
}`,
error instanceof Error ? error : undefined
);
}
}
/**
* Get storage information
*/
async getInfo(): Promise<{
bucket: string;
region?: string;
connected: boolean;
}> {
if (!this.client || !this.config) {
return {
bucket: "",
connected: false,
};
}
try {
// Test connection by listing objects (or another lightweight operation)
await this.client.upload(
{
key: "health-check",
body: "test",
contentType: "text/plain",
},
false
);
// Clean up test file
// Note: R2Client would need a delete method for this
return {
bucket: this.config.bucket,
region: this.config.region,
connected: true,
};
} catch {
return {
bucket: this.config.bucket,
region: this.config.region,
connected: false,
};
}
}
/**
* Generate a signed URL for temporary access
*/
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
if (!this.client) {
throw new StorageError("Storage not configured");
}
try {
return await this.client.getSignedUrl(key, expiresIn);
} catch (error) {
throw new StorageError(
`Failed to generate signed URL: ${
error instanceof Error ? error.message : String(error)
}`,
error instanceof Error ? error : undefined
);
}
}
/**
* Check if storage is configured and available
*/
isAvailable(): boolean {
return this.client !== null && this.config !== null;
}
/**
* Cleanup resources (if needed)
*/
async cleanup(): Promise<void> {
// The R2Client doesn't have persistent connections to clean up
// But this method is here for future use or if we add connection pooling
}
// Private methods
private loadFromEnvironment(): void {
const endpoint = process.env.R2_ENDPOINT || process.env.S3_ENDPOINT;
const accessKeyId =
process.env.R2_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID;
const secretAccessKey =
process.env.R2_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY;
const bucket = process.env.R2_BUCKET || process.env.S3_BUCKET;
const region = process.env.R2_REGION || process.env.AWS_REGION;
if (endpoint && accessKeyId && secretAccessKey && bucket) {
this.config = {
endpoint,
accessKeyId,
secretAccessKey,
bucket,
region,
};
this.client = new R2Client(this.config);
}
}
}