import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; // Environment variables for S3 configuration const S3_BUCKET_URL = process.env.MINIO_S3_UPLOAD_BUCKET_URL || 'https://dome-api.slm-lab.net/'; const S3_REGION = process.env.MINIO_AWS_REGION || 'eu-east-1'; const S3_BUCKET_NAME = process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME || 'pages'; const S3_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID; const S3_SECRET_KEY = process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY; // Basic client config without credentials const s3Config = { region: S3_REGION, endpoint: S3_BUCKET_URL, forcePathStyle: true, // Required for MinIO }; // Add credentials if available if (S3_ACCESS_KEY && S3_SECRET_KEY) { Object.assign(s3Config, { credentials: { accessKeyId: S3_ACCESS_KEY, secretAccessKey: S3_SECRET_KEY } }); } // Create S3 client with MinIO configuration export const s3Client = new S3Client(s3Config); // Helper functions for S3 operations // List objects in a "folder" for a specific user export async function listUserObjects(userId: string, folder: string) { try { const prefix = `user-${userId}/${folder}/`; const command = new ListObjectsV2Command({ Bucket: S3_BUCKET_NAME, 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 })) || []; } 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_BUCKET_NAME, 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, contentType?: string) { try { const command = new PutObjectCommand({ Bucket: S3_BUCKET_NAME, Key: key, Body: content, ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain') }); const response = await s3Client.send(command); return { id: key, title: key.split('/').pop()?.replace('.md', '') || '', lastModified: new Date().toISOString(), size: content.length, type: 'file', mime: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain'), etag: response.ETag }; } catch (error) { console.error('Error putting object:', error); throw error; } } // Delete object export async function deleteObject(key: string) { try { const command = new DeleteObjectCommand({ Bucket: S3_BUCKET_NAME, 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(`Starting folder structure creation for user: ${userId}`); // Define the standard folders to create - use lowercase for consistency with S3 operations // These are the canonical folder names that match what the frontend expects in the "vues" sidebar const folders = ['notes', 'diary', 'health', 'contacts']; // Also create capitalized versions for backward compatibility with UI components const capitalizedFolders = ['Notes', 'Diary', 'Health', 'Contacts']; // Results tracking const results = { lowercase: [] as string[], capitalized: [] as string[] }; // For S3, creating a folder means creating an empty object with the folder name as a prefix // First create lowercase versions (primary storage) for (const folder of folders) { try { const key = `user-${userId}/${folder}/`; console.log(`Creating folder: ${key}`); await putObject(key, '', 'application/x-directory'); results.lowercase.push(folder); // Create a placeholder file to ensure the folder is visible const placeholderKey = `user-${userId}/${folder}/.placeholder`; await putObject(placeholderKey, 'This is a placeholder file to ensure the folder exists.', 'text/plain'); } catch (error) { console.error(`Error creating lowercase folder ${folder}:`, error); } } // Then create capitalized versions (for backward compatibility) for (const folder of capitalizedFolders) { try { const key = `user-${userId}/${folder}/`; console.log(`Creating capitalized folder: ${key}`); await putObject(key, '', 'application/x-directory'); results.capitalized.push(folder); // Create a placeholder file to ensure the folder is visible const placeholderKey = `user-${userId}/${folder}/.placeholder`; await putObject(placeholderKey, 'This is a placeholder file to ensure the folder exists.', 'text/plain'); } catch (error) { console.error(`Error creating capitalized folder ${folder}:`, error); } } console.log(`Successfully created folder structure for user: ${userId}`, results); 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_BUCKET_NAME, Key: key }); return await getSignedUrl(s3Client, command, { expiresIn }); } catch (error) { console.error('Error generating presigned URL:', error); throw error; } }