From 312ed5894d9d9fc1117c2c335e65f33d99f2c0fa Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 16 Jan 2026 14:19:55 +0100 Subject: [PATCH] Pages corrections pages missions --- app/api/missions/[missionId]/files/route.ts | 200 ++++++++++---------- 1 file changed, 104 insertions(+), 96 deletions(-) diff --git a/app/api/missions/[missionId]/files/route.ts b/app/api/missions/[missionId]/files/route.ts index 6064b3f..773e63a 100644 --- a/app/api/missions/[missionId]/files/route.ts +++ b/app/api/missions/[missionId]/files/route.ts @@ -5,26 +5,20 @@ import { prisma } from '@/lib/prisma'; import { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { Readable } from 'stream'; -// S3 Configuration for missions bucket -// Use the same configuration as mission-uploads.ts for consistency -const MISSIONS_S3_CONFIG = { - endpoint: 'https://dome-api.slm-lab.net', - region: 'us-east-1', - bucket: 'missions', - accessKey: process.env.MINIO_ACCESS_KEY || '4aBT4CMb7JIMMyUtp4Pl', - secretKey: process.env.MINIO_SECRET_KEY || 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg' -}; - +// Use the exact same S3 client configuration as mission-uploads.ts and image route +// This ensures we use the same credentials and settings that work for other mission operations const missionsS3Client = new S3Client({ - region: MISSIONS_S3_CONFIG.region, - endpoint: MISSIONS_S3_CONFIG.endpoint, + region: 'us-east-1', + endpoint: 'https://dome-api.slm-lab.net', credentials: { - accessKeyId: MISSIONS_S3_CONFIG.accessKey, - secretAccessKey: MISSIONS_S3_CONFIG.secretKey + accessKeyId: '4aBT4CMb7JIMMyUtp4Pl', + secretAccessKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg' }, forcePathStyle: true // Required for MinIO }); +const MISSIONS_BUCKET = 'missions'; + // Helper function to check if user has access to mission async function checkMissionAccess(userId: string, missionId: string): Promise { const mission = await prisma.mission.findFirst({ @@ -71,96 +65,110 @@ export async function GET( const { searchParams } = new URL(request.url); const path = searchParams.get('path') || ''; // Subfolder path within mission - // Construct prefix for listing - // Based on mission-uploads.ts, files are stored in MinIO without the "missions/" prefix - // The filePath in DB is "missions/{missionId}/attachments/{filename}" - // But in MinIO it's stored as "{missionId}/attachments/{filename}" - // So we need to use just "{missionId}/" or "{missionId}/{path}/" as prefix - const prefix = path ? `${missionId}/${path}/` : `${missionId}/`; - - console.log(`[GET /api/missions/${missionId}/files] Listing with prefix: "${prefix}" in bucket: "${MISSIONS_S3_CONFIG.bucket}"`); - - const command = new ListObjectsV2Command({ - Bucket: MISSIONS_S3_CONFIG.bucket, - Prefix: prefix, - Delimiter: '/' + // Get attachments from database first (like the missions page does) + const attachments = await prisma.attachment.findMany({ + where: { missionId }, + select: { + id: true, + filename: true, + filePath: true, + fileType: true, + fileSize: true, + createdAt: true + }, + orderBy: { createdAt: 'desc' } }); - console.log(`[GET /api/missions/${missionId}/files] S3 command:`, { - bucket: MISSIONS_S3_CONFIG.bucket, - prefix: prefix, - endpoint: MISSIONS_S3_CONFIG.endpoint, - accessKey: MISSIONS_S3_CONFIG.accessKey.substring(0, 4) + '...' - }); - - let response; - try { - response = await missionsS3Client.send(command); - console.log(`[GET /api/missions/${missionId}/files] S3 response success:`, { - commonPrefixes: response.CommonPrefixes?.length || 0, - contents: response.Contents?.length || 0 - }); - } catch (s3Error: any) { - console.error(`[GET /api/missions/${missionId}/files] S3 error:`, { - code: s3Error.Code, - message: s3Error.message, - bucket: MISSIONS_S3_CONFIG.bucket, - prefix: prefix, - endpoint: MISSIONS_S3_CONFIG.endpoint, - fullError: s3Error - }); - throw s3Error; - } + // Try to list files from S3, but don't fail if it doesn't work + let s3Folders: any[] = []; + let s3Files: any[] = []; - // Extract folders (CommonPrefixes) - const folders = (response.CommonPrefixes || []).map(commonPrefix => { - const folderPath = commonPrefix.Prefix || ''; - // Extract folder name from path (e.g., "123/docs/" -> "docs") - const pathParts = folderPath.replace(prefix, '').split('/').filter(Boolean); - const folderName = pathParts[pathParts.length - 1] || folderPath; - // Store full path with missions/ prefix for consistency - const fullPath = `missions/${folderPath}`; - return { - type: 'folder', - name: folderName, - path: fullPath, - key: fullPath - }; - }); + try { + // Construct prefix for listing + // Based on mission-uploads.ts, files are stored in MinIO without the "missions/" prefix + // The filePath in DB is "missions/{missionId}/attachments/{filename}" + // But in MinIO it's stored as "{missionId}/attachments/{filename}" + const prefix = path ? `${missionId}/${path}/` : `${missionId}/`; - // Extract files (Contents, excluding folder markers) - const files = (response.Contents || []) - .filter(obj => { - if (!obj.Key) return false; - // Exclude folder markers - if (obj.Key.endsWith('/')) return false; - // Exclude placeholder files - if (obj.Key.includes('.placeholder')) return false; - return true; - }) - .map(obj => { - const key = obj.Key || ''; - // Store full path with missions/ prefix for consistency - const fullPath = `missions/${key}`; + console.log(`[GET /api/missions/${missionId}/files] Listing with prefix: "${prefix}" in bucket: "${MISSIONS_BUCKET}"`); + + const command = new ListObjectsV2Command({ + Bucket: MISSIONS_BUCKET, + Prefix: prefix, + Delimiter: '/' + }); + + const response = await missionsS3Client.send(command); + + // Extract folders (CommonPrefixes) + s3Folders = (response.CommonPrefixes || []).map(commonPrefix => { + const folderPath = commonPrefix.Prefix || ''; + const pathParts = folderPath.replace(prefix, '').split('/').filter(Boolean); + const folderName = pathParts[pathParts.length - 1] || folderPath; + const fullPath = `missions/${folderPath}`; return { - type: 'file', - name: key.split('/').pop() || key, + type: 'folder', + name: folderName, path: fullPath, - key: fullPath, - size: obj.Size, - lastModified: obj.LastModified + key: fullPath }; }); + // Extract files (Contents, excluding folder markers) + s3Files = (response.Contents || []) + .filter(obj => { + if (!obj.Key) return false; + if (obj.Key.endsWith('/')) return false; + if (obj.Key.includes('.placeholder')) return false; + return true; + }) + .map(obj => { + const key = obj.Key || ''; + const fullPath = `missions/${key}`; + return { + type: 'file', + name: key.split('/').pop() || key, + path: fullPath, + key: fullPath, + size: obj.Size, + lastModified: obj.LastModified + }; + }); + } catch (s3Error: any) { + console.warn(`[GET /api/missions/${missionId}/files] S3 listing failed, using database attachments only:`, { + code: s3Error.Code, + message: s3Error.message + }); + // Continue with database attachments only + } + + // Convert database attachments to file format + const dbFiles = attachments.map(att => ({ + type: 'file' as const, + name: att.filename, + path: att.filePath, + key: att.filePath, + size: att.fileSize, + lastModified: att.createdAt + })); + + // Combine S3 files and database attachments, removing duplicates + const allFiles = [...s3Files, ...dbFiles]; + const uniqueFiles = allFiles.filter((file, index, self) => + index === self.findIndex(f => f.key === file.key) + ); + + // Combine folders and files + const allItems = [...s3Folders, ...uniqueFiles].sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'folder' ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + return NextResponse.json({ - folders, - files: [...folders, ...files].sort((a, b) => { - // Folders first, then files, both alphabetically - if (a.type !== b.type) { - return a.type === 'folder' ? -1 : 1; - } - return a.name.localeCompare(b.name); - }) + folders: s3Folders, + files: allItems }); } catch (error) { console.error('Error listing mission files:', error); @@ -207,7 +215,7 @@ export async function POST( const minioKey = key.replace(/^missions\//, ''); const command = new GetObjectCommand({ - Bucket: MISSIONS_S3_CONFIG.bucket, + Bucket: MISSIONS_BUCKET, Key: minioKey }); @@ -260,7 +268,7 @@ export async function PUT( const minioKey = key.replace(/^missions\//, ''); const command = new PutObjectCommand({ - Bucket: MISSIONS_S3_CONFIG.bucket, + Bucket: MISSIONS_BUCKET, Key: minioKey, Body: content || Buffer.alloc(0), ContentType: 'text/plain'