208 lines
6.6 KiB
TypeScript
208 lines
6.6 KiB
TypeScript
import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
|
|
// S3 Configuration
|
|
// Uses existing MINIO_* environment variables that are shared across the application
|
|
export const S3_CONFIG = {
|
|
// Remove trailing slash from endpoint if present
|
|
endpoint: (process.env.MINIO_S3_UPLOAD_BUCKET_URL || process.env.S3_ENDPOINT || 'https://dome-api.slm-lab.net').replace(/\/$/, ''),
|
|
region: process.env.MINIO_AWS_REGION || process.env.S3_REGION || 'us-east-1',
|
|
// Use MINIO_AWS_S3_UPLOAD_BUCKET_NAME for pages bucket (shared with other parts of the app)
|
|
bucket: process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME || process.env.S3_BUCKET || 'pages',
|
|
accessKey: process.env.MINIO_ACCESS_KEY || process.env.S3_ACCESS_KEY || '',
|
|
secretKey: process.env.MINIO_SECRET_KEY || process.env.S3_SECRET_KEY || ''
|
|
};
|
|
|
|
// Validate required S3 configuration
|
|
if (!S3_CONFIG.accessKey || !S3_CONFIG.secretKey) {
|
|
console.error('⚠️ S3 credentials are missing! Please set MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables.');
|
|
if (process.env.NODE_ENV === 'production') {
|
|
throw new Error('S3 credentials are required in production environment');
|
|
}
|
|
}
|
|
|
|
// Initialize S3 client for Minio
|
|
export const s3Client = new S3Client({
|
|
region: S3_CONFIG.region,
|
|
endpoint: S3_CONFIG.endpoint,
|
|
credentials: {
|
|
accessKeyId: S3_CONFIG.accessKey,
|
|
secretAccessKey: S3_CONFIG.secretKey
|
|
},
|
|
forcePathStyle: true // Required for MinIO
|
|
});
|
|
|
|
/**
|
|
* Upload a file to S3
|
|
* Similar implementation to mission-uploads.ts which works correctly with MinIO
|
|
*/
|
|
export async function putObject(
|
|
key: string,
|
|
content: string | Buffer,
|
|
contentType?: string
|
|
): Promise<{ key: string; url?: string }> {
|
|
// Convert string to Buffer consistently, exactly like mission-uploads.ts
|
|
// This ensures the same encoding and buffer handling that works for mission attachments
|
|
// For empty strings, use an empty buffer instead of Buffer.from('') to avoid MinIO issues
|
|
let buffer: Buffer;
|
|
if (typeof content === 'string') {
|
|
buffer = content.length > 0
|
|
? Buffer.from(content, 'utf-8')
|
|
: Buffer.alloc(0);
|
|
} else if (Buffer.isBuffer(content)) {
|
|
buffer = content;
|
|
} else {
|
|
buffer = Buffer.from(content);
|
|
}
|
|
|
|
// Use the same pattern as mission-uploads.ts: direct s3Client.send with PutObjectCommand
|
|
await s3Client.send(new PutObjectCommand({
|
|
Bucket: S3_CONFIG.bucket,
|
|
Key: key,
|
|
Body: buffer,
|
|
ContentType: contentType || 'text/plain',
|
|
}));
|
|
|
|
return { key };
|
|
}
|
|
|
|
/**
|
|
* Get object content from S3
|
|
*/
|
|
export async function getObjectContent(key: string): Promise<string | null> {
|
|
try {
|
|
const command = new GetObjectCommand({
|
|
Bucket: S3_CONFIG.bucket,
|
|
Key: key,
|
|
});
|
|
|
|
const response = await s3Client.send(command);
|
|
if (!response.Body) {
|
|
return null;
|
|
}
|
|
|
|
const chunks: Uint8Array[] = [];
|
|
for await (const chunk of response.Body as any) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
const buffer = Buffer.concat(chunks);
|
|
return buffer.toString('utf-8');
|
|
} catch (error) {
|
|
console.error('Error getting object content:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an object from S3
|
|
*/
|
|
export async function deleteObject(key: string): Promise<void> {
|
|
const command = new DeleteObjectCommand({
|
|
Bucket: S3_CONFIG.bucket,
|
|
Key: key,
|
|
});
|
|
|
|
await s3Client.send(command);
|
|
}
|
|
|
|
/**
|
|
* List objects for a user in a specific folder
|
|
*/
|
|
export async function listUserObjects(userId: string, folder: string): Promise<Array<{ key: string; name: string; size?: number; lastModified?: Date }>> {
|
|
const prefix = `user-${userId}/${folder}/`;
|
|
const command = new ListObjectsV2Command({
|
|
Bucket: S3_CONFIG.bucket,
|
|
Prefix: prefix,
|
|
Delimiter: '/'
|
|
});
|
|
|
|
const response = await s3Client.send(command);
|
|
const objects = response.Contents || [];
|
|
|
|
return objects
|
|
.filter(obj => obj.Key && !obj.Key.endsWith('/') && !obj.Key.includes('.placeholder'))
|
|
.map(obj => ({
|
|
key: obj.Key!,
|
|
name: obj.Key!.split('/').pop() || obj.Key!,
|
|
size: obj.Size,
|
|
lastModified: obj.LastModified
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get public URL for an object
|
|
* Simple URL construction for MinIO/S3 objects
|
|
*/
|
|
export function getPublicUrl(filePath: string, bucket?: string): string {
|
|
if (!filePath) return '';
|
|
if (filePath.startsWith('http')) return filePath; // Already a full URL
|
|
|
|
// Remove leading slash if present
|
|
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
|
|
|
// Construct the full URL
|
|
const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present
|
|
const bucketName = bucket || S3_CONFIG.bucket;
|
|
|
|
// Return original path if no endpoint is configured
|
|
if (!endpoint) return cleanPath;
|
|
|
|
// Construct and return the full URL
|
|
return `${endpoint}/${bucketName}/${cleanPath}`;
|
|
}
|
|
|
|
/**
|
|
* Create standard folder structure for a user
|
|
* In S3/MinIO, folders don't need to be explicitly created - they exist as prefixes.
|
|
* We only create a placeholder file to mark the folder as initialized.
|
|
*/
|
|
export async function createUserFolderStructure(userId: string): Promise<void> {
|
|
const folders = ['notes', 'diary', 'health', 'contacts'];
|
|
|
|
for (const folder of folders) {
|
|
try {
|
|
// In S3/MinIO, folders are just prefixes, so we don't need to create an empty object.
|
|
// We only create a placeholder file to mark the folder as initialized.
|
|
const placeholderKey = `user-${userId}/${folder}/.placeholder`;
|
|
|
|
// Use a simple text content for the placeholder
|
|
const placeholderContent = `Folder initialized at ${new Date().toISOString()}`;
|
|
|
|
await putObject(placeholderKey, placeholderContent, 'text/plain');
|
|
|
|
console.log(`Created folder: user-${userId}/${folder}/`);
|
|
} catch (error) {
|
|
console.error(`Error creating folder ${folder} for user ${userId}:`, error);
|
|
// Continue with other folders even if one fails
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function uploadMissionFile({
|
|
missionId,
|
|
file,
|
|
type, // 'logo' or 'attachment'
|
|
}: {
|
|
missionId: string;
|
|
file: File;
|
|
type: 'logo' | 'attachment';
|
|
}): Promise<{ success: boolean; data?: any; error?: string }> {
|
|
const formData = new FormData();
|
|
formData.append('missionId', missionId);
|
|
formData.append('type', type);
|
|
formData.append('file', file);
|
|
|
|
const res = await fetch('/api/missions/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
return { success: false, error: err.error || 'Upload failed' };
|
|
}
|
|
|
|
const data = await res.json();
|
|
return { success: true, data };
|
|
}
|