import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; /** * S3/MinIO Configuration * * IMPORTANT: Set these environment variables in your .env file: * * MINIO_S3_UPLOAD_BUCKET_URL=https://your-minio-instance.com/ * MINIO_AWS_REGION=your-region * MINIO_AWS_S3_UPLOAD_BUCKET_NAME=your-bucket-name * MINIO_ACCESS_KEY=your-access-key * MINIO_SECRET_KEY=your-secret-key * * Alternative credentials (fallback): * AWS_ACCESS_KEY_ID=your-aws-access-key * AWS_SECRET_ACCESS_KEY=your-aws-secret-key */ export const S3_CONFIG = { endpoint: process.env.MINIO_S3_UPLOAD_BUCKET_URL, region: process.env.MINIO_AWS_REGION, bucket: process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME, accessKey: process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID, secretKey: process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY, } // Initialize S3 client with standard configuration const s3Config = { region: S3_CONFIG.region, endpoint: S3_CONFIG.endpoint, forcePathStyle: true, // Required for MinIO }; // Add credentials if available if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) { Object.assign(s3Config, { credentials: { accessKeyId: S3_CONFIG.accessKey, secretAccessKey: S3_CONFIG.secretKey } }); } // Create S3 client export const s3Client = new S3Client(s3Config); // Check Minio connection on startup (async () => { try { console.log('Testing Minio/S3 connection...'); // Try a simple operation to test the connection const command = new ListObjectsV2Command({ Bucket: S3_CONFIG.bucket, MaxKeys: 1 }); const response = await s3Client.send(command); console.log('Minio/S3 connection successful! Bucket exists and is accessible.'); console.log(`Bucket details: ${S3_CONFIG.bucket}, contains ${response.KeyCount || 0} objects`); } catch (error) { console.error('CRITICAL ERROR: Failed to connect to Minio/S3 server!'); console.error('File uploads will fail until this is resolved.'); console.error('Error details:', error); console.error('Please check your S3/Minio configuration and server status.'); } })(); // Check for required environment variables if (!S3_CONFIG.endpoint || !S3_CONFIG.bucket) { console.error('ERROR: Missing required S3 environment variables!'); console.error('Please make sure your .env file contains:'); console.error('- MINIO_S3_UPLOAD_BUCKET_URL'); console.error('- MINIO_AWS_S3_UPLOAD_BUCKET_NAME'); console.error('- MINIO_ACCESS_KEY or AWS_ACCESS_KEY_ID'); console.error('- MINIO_SECRET_KEY or AWS_SECRET_ACCESS_KEY'); } // Log configuration for debugging (without exposing credentials) console.log('S3 Configuration:', { endpoint: S3_CONFIG.endpoint || 'MISSING!', region: S3_CONFIG.region || 'MISSING!', bucket: S3_CONFIG.bucket || 'MISSING!', hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!', hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!', }); // Helper functions for S3 operations // List objects in a "folder" for a specific user export async function listUserObjects(userId: string, folder: string) { try { // Remove the 'pages/' prefix since it's already the bucket name const prefix = `user-${userId}/${folder}/`; console.log(`Listing objects with prefix: ${prefix}`); const command = new ListObjectsV2Command({ Bucket: S3_CONFIG.bucket, Prefix: prefix, Delimiter: '/' }); const response = await s3Client.send(command); // Transform S3 objects to match the expected format for the frontend // This ensures compatibility with existing NextCloud based components return response.Contents?.map(item => ({ id: item.Key, title: item.Key?.split('/').pop()?.replace('.md', '') || '', lastModified: item.LastModified?.toISOString(), size: item.Size, type: 'file', mime: item.Key?.endsWith('.md') ? 'text/markdown' : 'application/octet-stream', etag: item.ETag })) // Filter out placeholder files and empty directory markers .filter(item => !item.title.startsWith('.placeholder') && item.title !== '') || []; } catch (error) { console.error('Error listing objects:', error); throw error; } } // Get object content export async function getObjectContent(key: string) { try { const command = new GetObjectCommand({ Bucket: S3_CONFIG.bucket, Key: key }); const response = await s3Client.send(command); // Convert the stream to string return await response.Body?.transformToString(); } catch (error) { console.error('Error getting object content:', error); throw error; } } // Put object (create or update a file) export async function putObject(key: string, content: string | Buffer, contentType?: string) { try { console.log(`Attempting to upload to S3/Minio: ${key}`); if (!S3_CONFIG.bucket) { throw new Error('S3 bucket name is not configured'); } const command = new PutObjectCommand({ Bucket: S3_CONFIG.bucket, Key: key, Body: content, ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain') }); console.log(`S3 PutObject request prepared for ${key}`); const response = await s3Client.send(command); console.log(`S3 PutObject successful for ${key}, ETag: ${response.ETag}`); return { id: key, title: key.split('/').pop()?.replace('.md', '') || '', lastModified: new Date().toISOString(), size: typeof content === 'string' ? content.length : content.length, type: 'file', mime: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain'), etag: response.ETag }; } catch (error) { console.error(`Error putting object to S3/Minio (${key}):`, error); // Check for specific S3 errors if ((error as any)?.name === 'NoSuchBucket') { console.error(`Bucket "${S3_CONFIG.bucket}" does not exist. Please create it first.`); } throw error; } } // Delete object export async function deleteObject(key: string) { try { const command = new DeleteObjectCommand({ Bucket: S3_CONFIG.bucket, Key: key }); await s3Client.send(command); return true; } catch (error) { console.error('Error deleting object:', error); throw error; } } // Create folder structure (In S3, folders are just prefix notations) export async function createUserFolderStructure(userId: string) { try { console.log(`Creating folder structure for user: ${userId}`); // Define standard folders - use lowercase only for simplicity and consistency const folders = ['notes', 'diary', 'health', 'contacts']; // Create folders with placeholders const results = []; for (const folder of folders) { try { // Create the folder path (just a prefix in S3) // Remove the 'pages/' prefix since it's already the bucket name const key = `user-${userId}/${folder}/`; console.log(`Creating folder: ${key}`); await putObject(key, '', 'application/x-directory'); // Create a placeholder file to ensure the folder exists and is visible const placeholderKey = `user-${userId}/${folder}/.placeholder`; await putObject(placeholderKey, 'Folder placeholder', 'text/plain'); results.push(folder); } catch (error) { console.error(`Error creating folder ${folder}:`, error); } } console.log(`Successfully created ${results.length} folders for user ${userId}: ${results.join(', ')}`); return true; } catch (error) { console.error('Error creating folder structure:', error); throw error; } } // Generate pre-signed URL for direct browser upload (optional feature) export async function generatePresignedUrl(key: string, expiresIn = 3600) { try { const command = new PutObjectCommand({ Bucket: S3_CONFIG.bucket, Key: key }); return await getSignedUrl(s3Client, command, { expiresIn }); } catch (error) { console.error('Error generating presigned URL:', error); throw error; } } // Generate a public URL for a file stored in Minio/S3 export function getPublicUrl(filePath: string): string { if (!filePath) return ''; if (filePath.startsWith('http')) return filePath; // Already a full URL console.log('Generating public URL for:', filePath); // 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 bucket = S3_CONFIG.bucket; console.log('S3 Config for URL generation:', { endpoint, bucket, cleanPath }); // Return original path if no endpoint is configured if (!endpoint) { console.warn('No S3/Minio endpoint configured, returning original path'); return cleanPath; } // Construct and return the full URL const publicUrl = `${endpoint}/${bucket}/${cleanPath}`; console.log('Generated public URL:', publicUrl); return publicUrl; } // Test Minio connection - can be called from browser console export async function testMinioConnection() { console.log('=== Testing Minio Connection ==='); try { console.log('S3 Configuration:', { endpoint: S3_CONFIG.endpoint || 'MISSING!', region: S3_CONFIG.region || 'MISSING!', bucket: S3_CONFIG.bucket || 'MISSING!', hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!', hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!', }); // Try a simple operation console.log('Attempting to list objects...'); const command = new ListObjectsV2Command({ Bucket: S3_CONFIG.bucket || '', MaxKeys: 5 }); const response = await s3Client.send(command); console.log('Connection successful!'); console.log('Response:', response); const files = response.Contents || []; console.log(`Found ${files.length} files in bucket ${S3_CONFIG.bucket}:`); files.forEach((file, index) => { console.log(` ${index + 1}. ${file.Key} (${file.Size} bytes)`); }); console.log('=== Test completed successfully ==='); return { success: true, files: files.map(f => f.Key) }; } catch (error) { console.error('=== Test failed ==='); console.error('Error details:', error); return { success: false, error }; } } // Make testMinioConnection available globally if in browser if (typeof window !== 'undefined') { (window as any).testMinioConnection = testMinioConnection; // Also expose the getPublicUrl function (window as any).getMinioUrl = getPublicUrl; console.log('Minio test utilities available. Run window.testMinioConnection() to test Minio connection.'); }