NeahNew/lib/s3.ts
2025-05-06 12:07:27 +02:00

343 lines
11 KiB
TypeScript

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,
missionsBucket: process.env.MINIO_MISSIONS_BUCKET || 'missions',
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, bucketName?: 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;
// Special handling for paths that start with 'pages/'
if (cleanPath.startsWith('pages/')) {
// For paths with pages/ prefix, use a different URL format
const minioBaseUrl = process.env.NEXT_PUBLIC_MINIO_BASE_URL || process.env.MINIO_PUBLIC_URL;
if (minioBaseUrl) {
const trimmedBaseUrl = minioBaseUrl.replace(/\/$/, ''); // Remove trailing slash if present
const publicUrl = `${trimmedBaseUrl}/${cleanPath}`;
console.log('Generated special public URL for pages path:', publicUrl);
return publicUrl;
}
}
// Determine which bucket to use
const bucket = bucketName || S3_CONFIG.bucket;
// Construct the full URL using the standard approach
const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present
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.');
}