pages s3
This commit is contained in:
parent
6ef3d4791c
commit
1a6d0dd6bf
@ -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 });
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
});
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
83
lib/s3.ts
83
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
|
||||
});
|
||||
|
||||
|
||||
4
types/vcard-parser.d.ts
vendored
Normal file
4
types/vcard-parser.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'vcard-parser' {
|
||||
export function parse(vcard: string): any;
|
||||
export function format(vcard: any): string;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user