diff --git a/app/api/storage/status/route.ts b/app/api/storage/status/route.ts index ce1cea73..05099bf9 100644 --- a/app/api/storage/status/route.ts +++ b/app/api/storage/status/route.ts @@ -1,18 +1,16 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { S3Client, ListBucketsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; -import { createUserFolderStructure, listUserObjects } from '@/lib/s3'; +import { ListBucketsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; +import { createUserFolderStructure } from '@/lib/s3'; -// Import the configured S3 client and bucket name from lib/s3.ts -import { s3Client as configuredS3Client } from '@/lib/s3'; +// Import the configured S3 client and config from lib/s3.ts +import { s3Client } from '@/lib/s3'; +import { S3_CONFIG } from '@/lib/s3'; -// Get bucket name from environment -const S3_BUCKET_NAME = process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME || 'pages'; - -// Cache for folder lists -const folderCache = new Map(); -const CACHE_TTL = 5 * 60 * 1000; // 5 minutes +// Simple in-memory cache with consistent expiration +const CACHE_TTL = 2 * 60 * 1000; // 2 minutes +const folderCache = new Map(); export async function GET() { try { @@ -22,88 +20,87 @@ export async function GET() { } const userId = session.user.id; + const cacheKey = `folders:${userId}`; - // Check if we have cached folders for this user - const cachedData = folderCache.get(userId); + // Check cache first + const cachedData = folderCache.get(cacheKey); 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({ status: 'ready', folders: cachedData.folders }); } - // Check S3 connectivity using the configured client + // Verify S3 connectivity try { - // Simple check by listing buckets - await configuredS3Client.send(new ListBucketsCommand({})); + await s3Client.send(new ListBucketsCommand({})); } catch (error) { - console.error('S3 connectivity check failed:', error); + console.error('S3 connectivity issue:', error); return NextResponse.json({ - error: 'S3 storage service is not accessible', - status: 'error' + status: 'error', + error: 'Storage service is unavailable' }, { status: 503 }); } - // Direct approach - list all folders under the user prefix + // List folders in the user's path try { + // Using the user prefix to list all folders const prefix = `user-${userId}/`; - - // List all objects with this prefix to find folders const command = new ListObjectsV2Command({ - Bucket: S3_BUCKET_NAME, + Bucket: S3_CONFIG.bucket, Prefix: prefix, Delimiter: '/' }); - const response = await configuredS3Client.send(command); + const response = await s3Client.send(command); + const prefixes = response.CommonPrefixes || []; - // CommonPrefixes contains the folder paths - const userPrefixes = response.CommonPrefixes || []; + // Extract folder names and convert to display format + let folders = prefixes + .map(prefix => { + // Extract folder name from path (e.g., user-123/notes/ → notes) + const folderName = prefix.Prefix?.split('/')[1] || ''; + + // Format folder name for display (capitalize first letter) + return folderName.charAt(0).toUpperCase() + folderName.slice(1); + }) + .filter(Boolean); // Remove any empty strings - // Extract folder names from prefixes (e.g., "user-123/notes/" → "Notes") - let userFolders = userPrefixes - .map(prefix => prefix.Prefix?.split('/')[1]) - .filter(Boolean) as string[]; + console.log(`Found ${folders.length} folders for user ${userId}`); - console.log(`Found ${userFolders.length} folders for user ${userId}:`, userFolders); - - // If no folders found, create the standard structure - if (userFolders.length === 0) { - console.log(`No folders found for user ${userId}, creating standard structure`); + // If no folders, create the standard structure + if (folders.length === 0) { + console.log(`No folders found, creating structure for user ${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 - const formattedFolders = userFolders.map(folder => - folder.charAt(0).toUpperCase() + folder.slice(1) - ); - - console.log(`Returning formatted folders for user ${userId}:`, formattedFolders); - // Update cache - folderCache.set(userId, { - folders: formattedFolders, + // Update cache with the results + folderCache.set(cacheKey, { + folders, timestamp: Date.now() }); - + + // Return the folder list return NextResponse.json({ status: 'ready', - folders: formattedFolders + folders }); } catch (error) { - console.error('Error fetching user folders:', error); - return NextResponse.json({ - error: 'Failed to fetch folders', - status: 'error' + console.error('Error listing folders:', error); + return NextResponse.json({ + status: 'error', + error: 'Failed to list folders' }, { status: 500 }); } } catch (error) { - console.error('Error in storage status check:', error); + console.error('Status endpoint error:', error); return NextResponse.json({ - error: 'Internal server error', - status: 'error' + status: 'error', + error: 'Internal server error' }, { status: 500 }); } } \ No newline at end of file diff --git a/app/pages/page.tsx b/app/pages/page.tsx index fa2b7967..e87e8a78 100644 --- a/app/pages/page.tsx +++ b/app/pages/page.tsx @@ -196,9 +196,12 @@ export default function CarnetPage() { try { setIsLoadingContacts(true); + // Use lowercase for consistency + const folderLowercase = folder.toLowerCase(); + // First, check if we're looking at a specific VCF file 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) { const { content } = await response.json(); const contacts = parseVCardContent(content); @@ -209,22 +212,24 @@ export default function CarnetPage() { } } else { // 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) { 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 const parsedContacts = await Promise.all( vcfFiles.map(async (file: any) => { 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) { const { content } = await contentResponse.json(); const contacts = parseVCardContent(content); return contacts.map(contact => ({ ...contact, - group: file.basename.replace('.vcf', '') + group: (file.basename || file.title)?.replace('.vcf', '') })); } return []; @@ -252,12 +257,16 @@ export default function CarnetPage() { try { setIsLoading(true); - // Handle folder paths for S3 format - endpoint will try both cases - const response = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`); + // Convert folder name to lowercase for consistent storage access + 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) { 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); } else { console.error('Error fetching notes:', await response.text()); @@ -275,24 +284,20 @@ export default function CarnetPage() { const handleSaveNote = async (note: Note) => { try { setIsSaving(true); - // Construct API payload - ensure folder is properly set + // Construct API payload with lowercase folder name const payload = { id: note.id, title: note.title, content: note.content, - folder: selectedFolder.toLowerCase(), // Use lowercase for S3 consistency + folder: selectedFolder.toLowerCase(), // Use lowercase for storage consistency mime: "text/markdown" }; - // Use the API endpoint to save the note - const endpoint = note.id ? '/api/nextcloud/files' : '/api/nextcloud/files'; + // Use direct storage API endpoint + const endpoint = '/api/storage/files'; const method = note.id ? 'PUT' : 'POST'; - console.log(`Saving note to ${selectedFolder} using ${method}:`, { - id: note.id, - title: note.title, - folder: selectedFolder.toLowerCase() - }); + console.log(`Saving note to ${selectedFolder.toLowerCase()} using ${method}`); const response = await fetch(endpoint, { method, diff --git a/app/signin/page.tsx b/app/signin/page.tsx index 521d2989..52f71d51 100644 --- a/app/signin/page.tsx +++ b/app/signin/page.tsx @@ -14,13 +14,13 @@ export default function SignIn() { useEffect(() => { 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 () => { try { setInitializationStatus("initializing"); - const response = await fetch('/api/nextcloud/init', { + const response = await fetch('/api/storage/init', { method: 'POST' }); diff --git a/components/carnet/navigation.tsx b/components/carnet/navigation.tsx index d460a18a..a6acb045 100644 --- a/components/carnet/navigation.tsx +++ b/components/carnet/navigation.tsx @@ -79,12 +79,10 @@ export default function Navigation({ nextcloudFolders, onFolderSelect }: Navigat const fetchContactFiles = async () => { try { setIsLoadingContacts(true); - // Use the consistent folder name case that matches S3 structure - // The endpoint will try both cases, but we should prefer the consistent one - const response = await fetch('/api/nextcloud/files?folder=Contacts'); + // Use the direct storage API endpoint and consistent lowercase folder naming + const response = await fetch('/api/storage/files?folder=contacts'); if (response.ok) { const files = await response.json(); - // Only log the number of files received, not their contents console.log(`Received ${files.length} files from storage`); // Filter for VCF files and map to ContactFile interface const vcfFiles = files @@ -95,7 +93,6 @@ export default function Navigation({ nextcloudFolders, onFolderSelect }: Navigat basename: file.basename || file.title, lastmod: file.lastmod || file.lastModified })); - // Only log the number of VCF files processed console.log(`Processed ${vcfFiles.length} VCF files`); setContactFiles(vcfFiles); } else { diff --git a/lib/s3.ts b/lib/s3.ts index a4db33b8..025005eb 100644 --- a/lib/s3.ts +++ b/lib/s3.ts @@ -1,31 +1,33 @@ 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; +// Simplified S3 configuration with consistent naming +export const S3_CONFIG = { + endpoint: process.env.S3_ENDPOINT || 'https://dome-api.slm-lab.net/', + region: process.env.S3_REGION || 'eu-east-1', + bucket: process.env.S3_BUCKET || 'pages', + 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 = { - region: S3_REGION, - endpoint: S3_BUCKET_URL, + region: S3_CONFIG.region, + endpoint: S3_CONFIG.endpoint, forcePathStyle: true, // Required for MinIO }; // Add credentials if available -if (S3_ACCESS_KEY && S3_SECRET_KEY) { +if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) { Object.assign(s3Config, { credentials: { - accessKeyId: S3_ACCESS_KEY, - secretAccessKey: S3_SECRET_KEY + accessKeyId: S3_CONFIG.accessKey, + secretAccessKey: S3_CONFIG.secretKey } }); } -// Create S3 client with MinIO configuration +// Create S3 client export const s3Client = new S3Client(s3Config); // Helper functions for S3 operations @@ -35,7 +37,7 @@ export async function listUserObjects(userId: string, folder: string) { try { const prefix = `user-${userId}/${folder}/`; const command = new ListObjectsV2Command({ - Bucket: S3_BUCKET_NAME, + Bucket: S3_CONFIG.bucket, Prefix: prefix, Delimiter: '/' }); @@ -63,7 +65,7 @@ export async function listUserObjects(userId: string, folder: string) { export async function getObjectContent(key: string) { try { const command = new GetObjectCommand({ - Bucket: S3_BUCKET_NAME, + Bucket: S3_CONFIG.bucket, Key: key }); @@ -81,7 +83,7 @@ export async function getObjectContent(key: string) { export async function putObject(key: string, content: string, contentType?: string) { try { const command = new PutObjectCommand({ - Bucket: S3_BUCKET_NAME, + Bucket: S3_CONFIG.bucket, Key: key, Body: content, 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) { try { const command = new DeleteObjectCommand({ - Bucket: S3_BUCKET_NAME, + Bucket: S3_CONFIG.bucket, Key: key }); @@ -123,55 +125,32 @@ export async function deleteObject(key: string) { // 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}`); + console.log(`Creating folder structure 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 + // Define standard folders - use lowercase only for simplicity and consistency const folders = ['notes', 'diary', 'health', 'contacts']; - // Also create capitalized versions for backward compatibility with UI components - const capitalizedFolders = ['Notes', 'Diary', 'Health', 'Contacts']; + // Create folders with placeholders + 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) { try { + // Create the folder path (just a prefix in S3) 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 + // Create a placeholder file to ensure the folder exists and is visible 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) { - console.error(`Error creating lowercase folder ${folder}:`, error); + console.error(`Error creating 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); + console.log(`Successfully created ${results.length} folders for user ${userId}: ${results.join(', ')}`); return true; } catch (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) { try { const command = new PutObjectCommand({ - Bucket: S3_BUCKET_NAME, + Bucket: S3_CONFIG.bucket, Key: key }); diff --git a/types/vcard-parser.d.ts b/types/vcard-parser.d.ts new file mode 100644 index 00000000..fb0650bf --- /dev/null +++ b/types/vcard-parser.d.ts @@ -0,0 +1,4 @@ +declare module 'vcard-parser' { + export function parse(vcard: string): any; + export function format(vcard: any): string; +} \ No newline at end of file