cloudflare-image-mcp
Version:
Cloudflare Workers AI Image Generator MCP Server
226 lines • 9.11 kB
JavaScript
import { S3Client, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
import { BaseStorageProvider } from './base-provider.js';
import { createLogger } from '../../utils/logger.js';
export class S3StorageProvider extends BaseStorageProvider {
client;
bucket;
region;
endpoint;
cdnUrl;
logger = createLogger('S3 Storage');
constructor(config) {
super(config);
this.bucket = config.bucket;
this.region = config.region;
this.endpoint = config.endpoint;
this.cdnUrl = config.cdnUrl;
this.logger.debug('Initializing S3 Storage provider', {
bucket: this.bucket,
region: this.region,
endpoint: this.endpoint || 'default'
});
this.client = new S3Client({
region: this.region,
endpoint: this.endpoint,
credentials: config.accessKey && config.secretKey ? {
accessKeyId: config.accessKey,
secretAccessKey: config.secretKey
} : undefined,
forcePathStyle: true // Required for Cloudflare R2
});
}
async save(buffer, metadata) {
const date = metadata.timestamp.toISOString().split('T')[0];
const size = metadata.parameters?.size;
const filename = this.generateFilename(size);
const modelPath = this.generateModelPath(metadata.model, filename);
const key = `outputs/images/generations/${date}/${modelPath}`;
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: buffer,
ContentType: 'image/jpeg'
});
await this.client.send(command);
// Generate URL
let url;
if (this.cdnUrl) {
url = `${this.cdnUrl}/${key}`;
}
else if (this.endpoint?.includes('cloudflare')) {
// Cloudflare R2 public URL pattern
url = `https://${this.bucket}.${this.endpoint?.replace('https://', '')}/${key}`;
}
else {
// Standard S3 URL
url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`;
}
return {
url,
filename,
path: key,
size: buffer.length,
storageType: 's3',
metadata
};
}
async delete(filename) {
try {
// Find the key by searching for the filename
const key = await this.findFileKey(filename);
if (!key)
return false;
const command = new DeleteObjectCommand({
Bucket: this.bucket,
Key: key
});
await this.client.send(command);
return true;
}
catch {
return false;
}
}
async list(options = {}) {
const items = [];
try {
let continuationToken;
do {
const command = new ListObjectsV2Command({
Bucket: this.bucket,
Prefix: 'outputs/images/generations/',
ContinuationToken: continuationToken
});
const response = await this.client.send(command);
if (response.Contents) {
for (const object of response.Contents) {
if (!object.Key || !object.LastModified || !object.Size)
continue;
// Parse date and model from key path: outputs/images/generations/YYYY-MM-DD/model/filename.jpg
// More flexible regex to handle various image formats and edge cases
const pathMatch = object.Key.match(/outputs\/images\/generations\/(\d{4}-\d{2}-\d{2})\/([^/]+)\/([^/]+\.(jpg|jpeg|png|webp))$/i);
if (!pathMatch) {
this.logger.debug('Skipping file that doesn\'t match expected pattern', { key: object.Key });
continue;
}
const [, dateDir, modelDir, filename] = pathMatch;
if (!this.shouldIncludeDate(dateDir, options.dateRange))
continue;
if (!this.shouldIncludeFile(modelDir, filename, { size: object.Size, mtime: object.LastModified }, options))
continue;
// Generate URL
let url;
if (this.cdnUrl) {
url = `${this.cdnUrl}/${object.Key}`;
}
else if (this.endpoint?.includes('cloudflare')) {
url = `https://${this.bucket}.${this.endpoint?.replace('https://', '')}/${object.Key}`;
}
else {
url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/${object.Key}`;
}
items.push({
filename,
url,
size: object.Size,
createdAt: object.LastModified,
metadata: this.extractMetadataFromFile(object.Key, object.LastModified)
});
}
}
continuationToken = response.NextContinuationToken;
} while (continuationToken);
}
catch (error) {
console.error('Error listing S3 objects:', error);
}
return this.sortAndPaginate(items, options);
}
async getStatistics() {
const allFiles = await this.list();
const stats = {
totalFiles: allFiles.length,
totalSize: allFiles.reduce((sum, file) => sum + file.size, 0),
filesByModel: {},
filesByDate: {}
};
if (allFiles.length > 0) {
stats.oldestFile = new Date(Math.min(...allFiles.map(f => f.createdAt.getTime())));
stats.newestFile = new Date(Math.max(...allFiles.map(f => f.createdAt.getTime())));
}
for (const file of allFiles) {
const date = file.createdAt.toISOString().split('T')[0];
stats.filesByDate[date] = (stats.filesByDate[date] || 0) + 1;
const model = file.metadata?.model || 'unknown';
stats.filesByModel[model] = (stats.filesByModel[model] || 0) + 1;
}
return stats;
}
async findFileKey(filename) {
try {
let continuationToken;
do {
const command = new ListObjectsV2Command({
Bucket: this.bucket,
Prefix: 'outputs/images/generations/',
ContinuationToken: continuationToken
});
const response = await this.client.send(command);
if (response.Contents) {
for (const object of response.Contents) {
if (!object.Key)
continue;
// Check if this object ends with our filename
if (object.Key.endsWith(`/${filename}`)) {
return object.Key;
}
}
}
continuationToken = response.NextContinuationToken;
} while (continuationToken);
}
catch (error) {
console.error('Error finding file key:', error);
}
return null;
}
shouldIncludeDate(dateDir, dateRange) {
if (!dateRange)
return true;
const date = new Date(dateDir);
const startDate = new Date(dateRange.start);
const endDate = new Date(dateRange.end);
return date >= startDate && date <= endDate;
}
shouldIncludeFile(modelDir, filename, _stats, options) {
// Filter by model
if (options.model) {
const fileModel = this.extractModelFromFilename(`outputs/images/generations/2024-01-01/${modelDir}/${filename}`);
if (fileModel !== options.model)
return false;
}
return true;
}
sortAndPaginate(items, options) {
// Sort by creation date (newest first)
items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
// Apply pagination
if (options.offset || options.limit) {
const offset = options.offset || 0;
const limit = options.limit || items.length;
return items.slice(offset, offset + limit);
}
return items;
}
extractMetadataFromFile(filename, createdAt) {
const model = this.extractModelFromFilename(filename);
if (!model)
return undefined;
return {
prompt: '', // Not stored in filename
model: `/${model}`,
timestamp: createdAt
};
}
}
//# sourceMappingURL=s3-provider.js.map