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 { 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<string, { folders: string[], timestamp: number }>();
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 });
}
}

View File

@ -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,

View File

@ -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'
});

View File

@ -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 {

View File

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

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