This commit is contained in:
alma 2025-05-04 14:27:24 +02:00
parent 6ef3d4791c
commit 1a6d0dd6bf
6 changed files with 114 additions and 132 deletions

View File

@ -1,18 +1,16 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { S3Client, ListBucketsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; import { ListBucketsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
import { createUserFolderStructure, listUserObjects } from '@/lib/s3'; import { createUserFolderStructure } from '@/lib/s3';
// Import the configured S3 client and bucket name from lib/s3.ts // Import the configured S3 client and config from lib/s3.ts
import { s3Client as configuredS3Client } from '@/lib/s3'; import { s3Client } from '@/lib/s3';
import { S3_CONFIG } from '@/lib/s3';
// Get bucket name from environment // Simple in-memory cache with consistent expiration
const S3_BUCKET_NAME = process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME || 'pages'; const CACHE_TTL = 2 * 60 * 1000; // 2 minutes
const folderCache = new Map();
// Cache for folder lists
const folderCache = new Map<string, { folders: string[], timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
export async function GET() { export async function GET() {
try { try {
@ -22,88 +20,87 @@ export async function GET() {
} }
const userId = session.user.id; const userId = session.user.id;
const cacheKey = `folders:${userId}`;
// Check if we have cached folders for this user // Check cache first
const cachedData = folderCache.get(userId); const cachedData = folderCache.get(cacheKey);
if (cachedData && (Date.now() - cachedData.timestamp < CACHE_TTL)) { if (cachedData && (Date.now() - cachedData.timestamp < CACHE_TTL)) {
console.log(`Returning cached folders for user ${userId}:`, cachedData.folders); console.log(`Using cached folders for user ${userId}`);
return NextResponse.json({ return NextResponse.json({
status: 'ready', status: 'ready',
folders: cachedData.folders folders: cachedData.folders
}); });
} }
// Check S3 connectivity using the configured client // Verify S3 connectivity
try { try {
// Simple check by listing buckets await s3Client.send(new ListBucketsCommand({}));
await configuredS3Client.send(new ListBucketsCommand({}));
} catch (error) { } catch (error) {
console.error('S3 connectivity check failed:', error); console.error('S3 connectivity issue:', error);
return NextResponse.json({ return NextResponse.json({
error: 'S3 storage service is not accessible', status: 'error',
status: 'error' error: 'Storage service is unavailable'
}, { status: 503 }); }, { status: 503 });
} }
// Direct approach - list all folders under the user prefix // List folders in the user's path
try { try {
// Using the user prefix to list all folders
const prefix = `user-${userId}/`; const prefix = `user-${userId}/`;
// List all objects with this prefix to find folders
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: S3_BUCKET_NAME, Bucket: S3_CONFIG.bucket,
Prefix: prefix, Prefix: prefix,
Delimiter: '/' Delimiter: '/'
}); });
const response = await configuredS3Client.send(command); const response = await s3Client.send(command);
const prefixes = response.CommonPrefixes || [];
// CommonPrefixes contains the folder paths // Extract folder names and convert to display format
const userPrefixes = response.CommonPrefixes || []; let folders = prefixes
.map(prefix => {
// Extract folder name from path (e.g., user-123/notes/ → notes)
const folderName = prefix.Prefix?.split('/')[1] || '';
// Extract folder names from prefixes (e.g., "user-123/notes/" → "Notes") // Format folder name for display (capitalize first letter)
let userFolders = userPrefixes return folderName.charAt(0).toUpperCase() + folderName.slice(1);
.map(prefix => prefix.Prefix?.split('/')[1]) })
.filter(Boolean) as string[]; .filter(Boolean); // Remove any empty strings
console.log(`Found ${userFolders.length} folders for user ${userId}:`, userFolders); console.log(`Found ${folders.length} folders for user ${userId}`);
// If no folders found, create the standard structure // If no folders, create the standard structure
if (userFolders.length === 0) { if (folders.length === 0) {
console.log(`No folders found for user ${userId}, creating standard structure`); console.log(`No folders found, creating structure for user ${userId}`);
await createUserFolderStructure(userId); await createUserFolderStructure(userId);
userFolders = ['notes', 'diary', 'health', 'contacts'];
// Use standard folder list for display
folders = ['Notes', 'Diary', 'Health', 'Contacts'];
} }
// Convert to Pascal case for backwards compatibility with NextCloud // Update cache with the results
const formattedFolders = userFolders.map(folder => folderCache.set(cacheKey, {
folder.charAt(0).toUpperCase() + folder.slice(1) folders,
);
console.log(`Returning formatted folders for user ${userId}:`, formattedFolders);
// Update cache
folderCache.set(userId, {
folders: formattedFolders,
timestamp: Date.now() timestamp: Date.now()
}); });
// Return the folder list
return NextResponse.json({ return NextResponse.json({
status: 'ready', status: 'ready',
folders: formattedFolders folders
}); });
} catch (error) { } catch (error) {
console.error('Error fetching user folders:', error); console.error('Error listing folders:', error);
return NextResponse.json({ return NextResponse.json({
error: 'Failed to fetch folders', status: 'error',
status: 'error' error: 'Failed to list folders'
}, { status: 500 }); }, { status: 500 });
} }
} catch (error) { } catch (error) {
console.error('Error in storage status check:', error); console.error('Status endpoint error:', error);
return NextResponse.json({ return NextResponse.json({
error: 'Internal server error', status: 'error',
status: 'error' error: 'Internal server error'
}, { status: 500 }); }, { status: 500 });
} }
} }

View File

@ -196,9 +196,12 @@ export default function CarnetPage() {
try { try {
setIsLoadingContacts(true); setIsLoadingContacts(true);
// Use lowercase for consistency
const folderLowercase = folder.toLowerCase();
// First, check if we're looking at a specific VCF file // First, check if we're looking at a specific VCF file
if (folder.endsWith('.vcf')) { if (folder.endsWith('.vcf')) {
const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(`/files/cube-${session?.user?.id}/Private/Contacts/${folder}`)}`); const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(`user-${session?.user?.id}/${folderLowercase}/${folder}`)}`);
if (response.ok) { if (response.ok) {
const { content } = await response.json(); const { content } = await response.json();
const contacts = parseVCardContent(content); const contacts = parseVCardContent(content);
@ -209,22 +212,24 @@ export default function CarnetPage() {
} }
} else { } else {
// If not a VCF file, list all VCF files in the folder // If not a VCF file, list all VCF files in the folder
const response = await fetch(`/api/nextcloud/files?folder=${folder}`); const response = await fetch(`/api/storage/files?folder=${folderLowercase}`);
if (response.ok) { if (response.ok) {
const files = await response.json(); const files = await response.json();
const vcfFiles = files.filter((file: any) => file.basename.endsWith('.vcf')); const vcfFiles = files.filter((file: any) =>
file.basename?.endsWith('.vcf') || file.title?.endsWith('.vcf')
);
// Parse VCF files and extract contact information // Parse VCF files and extract contact information
const parsedContacts = await Promise.all( const parsedContacts = await Promise.all(
vcfFiles.map(async (file: any) => { vcfFiles.map(async (file: any) => {
try { try {
const contentResponse = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(file.filename)}`); const contentResponse = await fetch(`/api/storage/files/content?path=${encodeURIComponent(file.id)}`);
if (contentResponse.ok) { if (contentResponse.ok) {
const { content } = await contentResponse.json(); const { content } = await contentResponse.json();
const contacts = parseVCardContent(content); const contacts = parseVCardContent(content);
return contacts.map(contact => ({ return contacts.map(contact => ({
...contact, ...contact,
group: file.basename.replace('.vcf', '') group: (file.basename || file.title)?.replace('.vcf', '')
})); }));
} }
return []; return [];
@ -252,12 +257,16 @@ export default function CarnetPage() {
try { try {
setIsLoading(true); setIsLoading(true);
// Handle folder paths for S3 format - endpoint will try both cases // Convert folder name to lowercase for consistent storage access
const response = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`); const folderLowercase = selectedFolder.toLowerCase();
console.log(`Fetching notes from folder: ${folderLowercase}`);
// Use direct storage API instead of adapter
const response = await fetch(`/api/storage/files?folder=${folderLowercase}`);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
console.log(`Fetched ${data.length} notes from ${selectedFolder} folder`); console.log(`Fetched ${data.length} notes from ${folderLowercase} folder`);
setNotes(data); setNotes(data);
} else { } else {
console.error('Error fetching notes:', await response.text()); console.error('Error fetching notes:', await response.text());
@ -275,24 +284,20 @@ export default function CarnetPage() {
const handleSaveNote = async (note: Note) => { const handleSaveNote = async (note: Note) => {
try { try {
setIsSaving(true); setIsSaving(true);
// Construct API payload - ensure folder is properly set // Construct API payload with lowercase folder name
const payload = { const payload = {
id: note.id, id: note.id,
title: note.title, title: note.title,
content: note.content, content: note.content,
folder: selectedFolder.toLowerCase(), // Use lowercase for S3 consistency folder: selectedFolder.toLowerCase(), // Use lowercase for storage consistency
mime: "text/markdown" mime: "text/markdown"
}; };
// Use the API endpoint to save the note // Use direct storage API endpoint
const endpoint = note.id ? '/api/nextcloud/files' : '/api/nextcloud/files'; const endpoint = '/api/storage/files';
const method = note.id ? 'PUT' : 'POST'; const method = note.id ? 'PUT' : 'POST';
console.log(`Saving note to ${selectedFolder} using ${method}:`, { console.log(`Saving note to ${selectedFolder.toLowerCase()} using ${method}`);
id: note.id,
title: note.title,
folder: selectedFolder.toLowerCase()
});
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method, method,

View File

@ -14,13 +14,13 @@ export default function SignIn() {
useEffect(() => { useEffect(() => {
if (session?.user) { if (session?.user) {
console.log("Session available, checking storage initialization"); console.log("Session available, initializing storage");
// Initialize storage // Initialize storage using direct API
const initializeStorage = async () => { const initializeStorage = async () => {
try { try {
setInitializationStatus("initializing"); setInitializationStatus("initializing");
const response = await fetch('/api/nextcloud/init', { const response = await fetch('/api/storage/init', {
method: 'POST' method: 'POST'
}); });

View File

@ -79,12 +79,10 @@ export default function Navigation({ nextcloudFolders, onFolderSelect }: Navigat
const fetchContactFiles = async () => { const fetchContactFiles = async () => {
try { try {
setIsLoadingContacts(true); setIsLoadingContacts(true);
// Use the consistent folder name case that matches S3 structure // Use the direct storage API endpoint and consistent lowercase folder naming
// The endpoint will try both cases, but we should prefer the consistent one const response = await fetch('/api/storage/files?folder=contacts');
const response = await fetch('/api/nextcloud/files?folder=Contacts');
if (response.ok) { if (response.ok) {
const files = await response.json(); const files = await response.json();
// Only log the number of files received, not their contents
console.log(`Received ${files.length} files from storage`); console.log(`Received ${files.length} files from storage`);
// Filter for VCF files and map to ContactFile interface // Filter for VCF files and map to ContactFile interface
const vcfFiles = files const vcfFiles = files
@ -95,7 +93,6 @@ export default function Navigation({ nextcloudFolders, onFolderSelect }: Navigat
basename: file.basename || file.title, basename: file.basename || file.title,
lastmod: file.lastmod || file.lastModified lastmod: file.lastmod || file.lastModified
})); }));
// Only log the number of VCF files processed
console.log(`Processed ${vcfFiles.length} VCF files`); console.log(`Processed ${vcfFiles.length} VCF files`);
setContactFiles(vcfFiles); setContactFiles(vcfFiles);
} else { } else {

View File

@ -1,31 +1,33 @@
import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
// Environment variables for S3 configuration // Simplified S3 configuration with consistent naming
const S3_BUCKET_URL = process.env.MINIO_S3_UPLOAD_BUCKET_URL || 'https://dome-api.slm-lab.net/'; export const S3_CONFIG = {
const S3_REGION = process.env.MINIO_AWS_REGION || 'eu-east-1'; endpoint: process.env.S3_ENDPOINT || 'https://dome-api.slm-lab.net/',
const S3_BUCKET_NAME = process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME || 'pages'; region: process.env.S3_REGION || 'eu-east-1',
const S3_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID; bucket: process.env.S3_BUCKET || 'pages',
const S3_SECRET_KEY = process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY; accessKey: process.env.S3_ACCESS_KEY,
secretKey: process.env.S3_SECRET_KEY,
}
// Basic client config without credentials // Initialize S3 client with standard configuration
const s3Config = { const s3Config = {
region: S3_REGION, region: S3_CONFIG.region,
endpoint: S3_BUCKET_URL, endpoint: S3_CONFIG.endpoint,
forcePathStyle: true, // Required for MinIO forcePathStyle: true, // Required for MinIO
}; };
// Add credentials if available // Add credentials if available
if (S3_ACCESS_KEY && S3_SECRET_KEY) { if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) {
Object.assign(s3Config, { Object.assign(s3Config, {
credentials: { credentials: {
accessKeyId: S3_ACCESS_KEY, accessKeyId: S3_CONFIG.accessKey,
secretAccessKey: S3_SECRET_KEY secretAccessKey: S3_CONFIG.secretKey
} }
}); });
} }
// Create S3 client with MinIO configuration // Create S3 client
export const s3Client = new S3Client(s3Config); export const s3Client = new S3Client(s3Config);
// Helper functions for S3 operations // Helper functions for S3 operations
@ -35,7 +37,7 @@ export async function listUserObjects(userId: string, folder: string) {
try { try {
const prefix = `user-${userId}/${folder}/`; const prefix = `user-${userId}/${folder}/`;
const command = new ListObjectsV2Command({ const command = new ListObjectsV2Command({
Bucket: S3_BUCKET_NAME, Bucket: S3_CONFIG.bucket,
Prefix: prefix, Prefix: prefix,
Delimiter: '/' Delimiter: '/'
}); });
@ -63,7 +65,7 @@ export async function listUserObjects(userId: string, folder: string) {
export async function getObjectContent(key: string) { export async function getObjectContent(key: string) {
try { try {
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: S3_BUCKET_NAME, Bucket: S3_CONFIG.bucket,
Key: key Key: key
}); });
@ -81,7 +83,7 @@ export async function getObjectContent(key: string) {
export async function putObject(key: string, content: string, contentType?: string) { export async function putObject(key: string, content: string, contentType?: string) {
try { try {
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: S3_BUCKET_NAME, Bucket: S3_CONFIG.bucket,
Key: key, Key: key,
Body: content, Body: content,
ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain') ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain')
@ -108,7 +110,7 @@ export async function putObject(key: string, content: string, contentType?: stri
export async function deleteObject(key: string) { export async function deleteObject(key: string) {
try { try {
const command = new DeleteObjectCommand({ const command = new DeleteObjectCommand({
Bucket: S3_BUCKET_NAME, Bucket: S3_CONFIG.bucket,
Key: key Key: key
}); });
@ -123,55 +125,32 @@ export async function deleteObject(key: string) {
// Create folder structure (In S3, folders are just prefix notations) // Create folder structure (In S3, folders are just prefix notations)
export async function createUserFolderStructure(userId: string) { export async function createUserFolderStructure(userId: string) {
try { try {
console.log(`Starting folder structure creation for user: ${userId}`); console.log(`Creating folder structure for user: ${userId}`);
// Define the standard folders to create - use lowercase for consistency with S3 operations // Define standard folders - use lowercase only for simplicity and consistency
// These are the canonical folder names that match what the frontend expects in the "vues" sidebar
const folders = ['notes', 'diary', 'health', 'contacts']; const folders = ['notes', 'diary', 'health', 'contacts'];
// Also create capitalized versions for backward compatibility with UI components // Create folders with placeholders
const capitalizedFolders = ['Notes', 'Diary', 'Health', 'Contacts']; const results = [];
// 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) { for (const folder of folders) {
try { try {
// Create the folder path (just a prefix in S3)
const key = `user-${userId}/${folder}/`; const key = `user-${userId}/${folder}/`;
console.log(`Creating folder: ${key}`); console.log(`Creating folder: ${key}`);
await putObject(key, '', 'application/x-directory'); await putObject(key, '', 'application/x-directory');
results.lowercase.push(folder);
// Create a placeholder file to ensure the folder is visible // Create a placeholder file to ensure the folder exists and is visible
const placeholderKey = `user-${userId}/${folder}/.placeholder`; const placeholderKey = `user-${userId}/${folder}/.placeholder`;
await putObject(placeholderKey, 'This is a placeholder file to ensure the folder exists.', 'text/plain'); await putObject(placeholderKey, 'Folder placeholder', 'text/plain');
results.push(folder);
} catch (error) { } catch (error) {
console.error(`Error creating lowercase folder ${folder}:`, error); console.error(`Error creating folder ${folder}:`, error);
} }
} }
// Then create capitalized versions (for backward compatibility) console.log(`Successfully created ${results.length} folders for user ${userId}: ${results.join(', ')}`);
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; return true;
} catch (error) { } catch (error) {
console.error('Error creating folder structure:', error); console.error('Error creating folder structure:', error);
@ -183,7 +162,7 @@ export async function createUserFolderStructure(userId: string) {
export async function generatePresignedUrl(key: string, expiresIn = 3600) { export async function generatePresignedUrl(key: string, expiresIn = 3600) {
try { try {
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: S3_BUCKET_NAME, Bucket: S3_CONFIG.bucket,
Key: key Key: key
}); });

4
types/vcard-parser.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'vcard-parser' {
export function parse(vcard: string): any;
export function format(vcard: any): string;
}