pages s3
This commit is contained in:
parent
f657d62373
commit
6ef3d4791c
@ -1,12 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3';
|
||||
import { S3Client, ListBucketsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
||||
import { createUserFolderStructure, listUserObjects } from '@/lib/s3';
|
||||
|
||||
// Import the configured S3 client from lib/s3.ts
|
||||
// Import the configured S3 client and bucket name from lib/s3.ts
|
||||
import { s3Client as configuredS3Client } 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
|
||||
@ -23,6 +26,7 @@ export async function GET() {
|
||||
// Check if we have cached folders for this user
|
||||
const cachedData = folderCache.get(userId);
|
||||
if (cachedData && (Date.now() - cachedData.timestamp < CACHE_TTL)) {
|
||||
console.log(`Returning cached folders for user ${userId}:`, cachedData.folders);
|
||||
return NextResponse.json({
|
||||
status: 'ready',
|
||||
folders: cachedData.folders
|
||||
@ -41,28 +45,34 @@ export async function GET() {
|
||||
}, { status: 503 });
|
||||
}
|
||||
|
||||
// List the user's base folders
|
||||
// Direct approach - list all folders under the user prefix
|
||||
try {
|
||||
// Standard folder list for the user
|
||||
const standardFolders = ['notes', 'diary', 'health', 'contacts'];
|
||||
let userFolders: string[] = [];
|
||||
|
||||
// Try to list existing folders
|
||||
for (const folder of standardFolders) {
|
||||
try {
|
||||
const files = await listUserObjects(userId, folder);
|
||||
if (files.length > 0 || folder === 'notes') {
|
||||
userFolders.push(folder);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking folder ${folder}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = `user-${userId}/`;
|
||||
|
||||
// List all objects with this prefix to find folders
|
||||
const command = new ListObjectsV2Command({
|
||||
Bucket: S3_BUCKET_NAME,
|
||||
Prefix: prefix,
|
||||
Delimiter: '/'
|
||||
});
|
||||
|
||||
const response = await configuredS3Client.send(command);
|
||||
|
||||
// CommonPrefixes contains the folder paths
|
||||
const userPrefixes = response.CommonPrefixes || [];
|
||||
|
||||
// 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 ${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`);
|
||||
await createUserFolderStructure(userId);
|
||||
userFolders = standardFolders;
|
||||
userFolders = ['notes', 'diary', 'health', 'contacts'];
|
||||
}
|
||||
|
||||
// Convert to Pascal case for backwards compatibility with NextCloud
|
||||
@ -70,6 +80,8 @@ export async function GET() {
|
||||
folder.charAt(0).toUpperCase() + folder.slice(1)
|
||||
);
|
||||
|
||||
console.log(`Returning formatted folders for user ${userId}:`, formattedFolders);
|
||||
|
||||
// Update cache
|
||||
folderCache.set(userId, {
|
||||
folders: formattedFolders,
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function SignIn() {
|
||||
const { data: session } = useSession();
|
||||
const [initializationStatus, setInitializationStatus] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger Keycloak sign-in
|
||||
@ -12,17 +13,36 @@ export default function SignIn() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user && !session.user.nextcloudInitialized) {
|
||||
// Initialize Nextcloud
|
||||
fetch('/api/nextcloud/init', {
|
||||
method: 'POST'
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
console.error('Failed to initialize Nextcloud');
|
||||
if (session?.user) {
|
||||
console.log("Session available, checking storage initialization");
|
||||
|
||||
// Initialize storage
|
||||
const initializeStorage = async () => {
|
||||
try {
|
||||
setInitializationStatus("initializing");
|
||||
const response = await fetch('/api/nextcloud/init', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to initialize storage:', await response.text());
|
||||
setInitializationStatus("failed");
|
||||
} else {
|
||||
console.log('Storage initialized successfully');
|
||||
setInitializationStatus("success");
|
||||
|
||||
// Force reload to ensure session is updated
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing storage:', error);
|
||||
setInitializationStatus("failed");
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error initializing Nextcloud:', error);
|
||||
});
|
||||
};
|
||||
|
||||
initializeStorage();
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
@ -36,11 +56,22 @@ export default function SignIn() {
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="w-full max-w-md space-y-8 bg-white/90 backdrop-blur-sm p-8 rounded-xl shadow-xl">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-white">
|
||||
Redirecting to login...
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||
{initializationStatus === "initializing"
|
||||
? "Initialisation de votre espace..."
|
||||
: initializationStatus === "success"
|
||||
? "Initialisation réussie, redirection..."
|
||||
: initializationStatus === "failed"
|
||||
? "Échec de l'initialisation. Veuillez réessayer."
|
||||
: "Redirection vers la page de connexion..."}
|
||||
</h2>
|
||||
{initializationStatus === "initializing" && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -67,12 +67,14 @@ export default function Navigation({ nextcloudFolders, onFolderSelect }: Navigat
|
||||
}
|
||||
};
|
||||
|
||||
// Sort folders according to the specified order
|
||||
const sortedFolders = [...nextcloudFolders].sort((a, b) => {
|
||||
const orderA = (FOLDER_CONFIG[a as FolderType]?.order) || 999;
|
||||
const orderB = (FOLDER_CONFIG[b as FolderType]?.order) || 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
// Make sure the sortedFolders logic can handle both empty arrays and other issues
|
||||
const sortedFolders = nextcloudFolders && nextcloudFolders.length > 0
|
||||
? [...nextcloudFolders].sort((a, b) => {
|
||||
const orderA = (FOLDER_CONFIG[a as FolderType]?.order) || 999;
|
||||
const orderB = (FOLDER_CONFIG[b as FolderType]?.order) || 999;
|
||||
return orderA - orderB;
|
||||
})
|
||||
: ['Notes', 'Diary', 'Health', 'Contacts']; // Default folders if none returned from API
|
||||
|
||||
const fetchContactFiles = async () => {
|
||||
try {
|
||||
@ -201,62 +203,68 @@ export default function Navigation({ nextcloudFolders, onFolderSelect }: Navigat
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<h2 className="text-xs font-semibold text-carnet-text-primary mb-2">VUES</h2>
|
||||
<div className="space-y-1">
|
||||
{sortedFolders.map((folder) => {
|
||||
const config = FOLDER_CONFIG[folder as FolderType];
|
||||
const Icon = config?.icon || FileText;
|
||||
const displayName = config?.displayName || folder;
|
||||
|
||||
return (
|
||||
<div key={folder}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (folder === 'Contacts') {
|
||||
setExpandedContacts(!expandedContacts);
|
||||
} else {
|
||||
onFolderSelect(folder);
|
||||
}
|
||||
}}
|
||||
className="w-full flex items-center px-2 py-1.5 text-sm rounded-md text-carnet-text-primary hover:bg-carnet-hover"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="ml-2 truncate">{displayName}</span>
|
||||
{folder === 'Contacts' && (
|
||||
<ChevronRight
|
||||
className={`h-3.5 w-3.5 flex-shrink-0 ml-auto transition-transform ${
|
||||
expandedContacts ? 'transform rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{folder === 'Contacts' && expandedContacts && (
|
||||
<div className="ml-4 mt-1 space-y-1">
|
||||
{isLoadingContacts ? (
|
||||
<div className="px-3 py-2 text-sm text-carnet-text-muted">Chargement...</div>
|
||||
) : contactFiles.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-carnet-text-muted">Aucun contact</div>
|
||||
) : (
|
||||
contactFiles.map((file) => {
|
||||
return (
|
||||
<button
|
||||
key={file.id}
|
||||
onClick={() => {
|
||||
// When clicking a VCF file, we want to select the Contacts folder
|
||||
// and pass the file information to the parent component
|
||||
onFolderSelect('Contacts');
|
||||
// You might want to add a callback here to handle the selected contact
|
||||
}}
|
||||
className="w-full flex items-center space-x-2 px-3 py-2 text-sm rounded-md text-carnet-text-muted hover:bg-carnet-hover"
|
||||
>
|
||||
<span>{file.basename.replace('.vcf', '')}</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
{sortedFolders.length === 0 ? (
|
||||
<div className="text-xs text-carnet-text-muted py-1 px-2">
|
||||
Chargement des dossiers...
|
||||
</div>
|
||||
) : (
|
||||
sortedFolders.map((folder) => {
|
||||
const config = FOLDER_CONFIG[folder as FolderType];
|
||||
const Icon = config?.icon || FileText;
|
||||
const displayName = config?.displayName || folder;
|
||||
|
||||
return (
|
||||
<div key={folder}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (folder === 'Contacts') {
|
||||
setExpandedContacts(!expandedContacts);
|
||||
} else {
|
||||
onFolderSelect(folder);
|
||||
}
|
||||
}}
|
||||
className="w-full flex items-center px-2 py-1.5 text-sm rounded-md text-carnet-text-primary hover:bg-carnet-hover"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="ml-2 truncate">{displayName}</span>
|
||||
{folder === 'Contacts' && (
|
||||
<ChevronRight
|
||||
className={`h-3.5 w-3.5 flex-shrink-0 ml-auto transition-transform ${
|
||||
expandedContacts ? 'transform rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</button>
|
||||
{folder === 'Contacts' && expandedContacts && (
|
||||
<div className="ml-4 mt-1 space-y-1">
|
||||
{isLoadingContacts ? (
|
||||
<div className="px-3 py-2 text-sm text-carnet-text-muted">Chargement...</div>
|
||||
) : contactFiles.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-carnet-text-muted">Aucun contact</div>
|
||||
) : (
|
||||
contactFiles.map((file) => {
|
||||
return (
|
||||
<button
|
||||
key={file.id}
|
||||
onClick={() => {
|
||||
// When clicking a VCF file, we want to select the Contacts folder
|
||||
// and pass the file information to the parent component
|
||||
onFolderSelect('Contacts');
|
||||
// You might want to add a callback here to handle the selected contact
|
||||
}}
|
||||
className="w-full flex items-center space-x-2 px-3 py-2 text-sm rounded-md text-carnet-text-muted hover:bg-carnet-hover"
|
||||
>
|
||||
<span>{file.basename.replace('.vcf', '')}</span>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
40
lib/s3.ts
40
lib/s3.ts
@ -123,6 +123,8 @@ 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}`);
|
||||
|
||||
// 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'];
|
||||
@ -130,22 +132,46 @@ export async function createUserFolderStructure(userId: string) {
|
||||
// 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) {
|
||||
const key = `user-${userId}/${folder}/`;
|
||||
console.log(`Creating folder: ${key}`);
|
||||
await putObject(key, '', 'application/x-directory');
|
||||
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) {
|
||||
const key = `user-${userId}/${folder}/`;
|
||||
console.log(`Creating capitalized folder: ${key}`);
|
||||
await putObject(key, '', 'application/x-directory');
|
||||
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}`);
|
||||
console.log(`Successfully created folder structure for user: ${userId}`, results);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating folder structure:', error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user