NeahNew/lib/s3.ts
2025-05-04 14:20:40 +02:00

195 lines
6.4 KiB
TypeScript

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;
}
}