missions carrousse
This commit is contained in:
parent
f2300638c3
commit
76d4d55285
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from "@/app/api/auth/options";
|
import { authOptions } from "@/app/api/auth/options";
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { deleteMissionLogo } from '@/lib/mission-uploads';
|
import { deleteMissionLogo } from '@/lib/mission-uploads';
|
||||||
|
import { getPublicUrl } from '@/lib/s3';
|
||||||
|
|
||||||
// Helper function to check authentication
|
// Helper function to check authentication
|
||||||
async function checkAuth(request: Request) {
|
async function checkAuth(request: Request) {
|
||||||
@ -77,8 +78,18 @@ export async function GET(request: Request, props: { params: Promise<{ missionId
|
|||||||
if (!mission) {
|
if (!mission) {
|
||||||
return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 });
|
return NextResponse.json({ error: 'Mission not found or access denied' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add public URLs to mission logo and attachments
|
||||||
|
const missionWithUrls = {
|
||||||
|
...mission,
|
||||||
|
logoUrl: mission.logo ? getPublicUrl(mission.logo) : null,
|
||||||
|
attachments: mission.attachments.map((attachment: { id: string; filename: string; filePath: string; fileType: string; fileSize: number; createdAt: Date }) => ({
|
||||||
|
...attachment,
|
||||||
|
publicUrl: getPublicUrl(attachment.filePath)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
return NextResponse.json(mission);
|
return NextResponse.json(missionWithUrls);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error retrieving mission:', error);
|
console.error('Error retrieving mission:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
generateMissionLogoUploadUrl,
|
generateMissionLogoUploadUrl,
|
||||||
generateMissionAttachmentUploadUrl
|
generateMissionAttachmentUploadUrl
|
||||||
} from '@/lib/mission-uploads';
|
} from '@/lib/mission-uploads';
|
||||||
|
import { getPublicUrl } from '@/lib/s3';
|
||||||
|
|
||||||
// Helper function to check authentication
|
// Helper function to check authentication
|
||||||
async function checkAuth(request: Request) {
|
async function checkAuth(request: Request) {
|
||||||
@ -84,19 +85,35 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
// Handle file upload (server-side)
|
// Handle file upload (server-side)
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
console.log('=== File upload request received ===');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Checking authentication...');
|
||||||
const { authorized, userId } = await checkAuth(request);
|
const { authorized, userId } = await checkAuth(request);
|
||||||
if (!authorized || !userId) {
|
if (!authorized || !userId) {
|
||||||
|
console.log('Authentication failed');
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
console.log('User authenticated:', userId);
|
||||||
|
|
||||||
// Parse the form data
|
// Parse the form data
|
||||||
|
console.log('Parsing form data...');
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const missionId = formData.get('missionId') as string;
|
const missionId = formData.get('missionId') as string;
|
||||||
const type = formData.get('type') as string; // 'logo' or 'attachment'
|
const type = formData.get('type') as string; // 'logo' or 'attachment'
|
||||||
const file = formData.get('file') as File;
|
const file = formData.get('file') as File;
|
||||||
|
|
||||||
|
console.log('Form data received:', {
|
||||||
|
missionId,
|
||||||
|
type,
|
||||||
|
fileExists: !!file,
|
||||||
|
fileName: file?.name,
|
||||||
|
fileSize: file?.size,
|
||||||
|
fileType: file?.type
|
||||||
|
});
|
||||||
|
|
||||||
if (!missionId || !type || !file) {
|
if (!missionId || !type || !file) {
|
||||||
|
console.log('Missing required fields:', { missionId: !!missionId, type: !!type, file: !!file });
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: 'Missing required fields',
|
error: 'Missing required fields',
|
||||||
required: { missionId: true, type: true, file: true },
|
required: { missionId: true, type: true, file: true },
|
||||||
@ -105,69 +122,111 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify that the mission exists and user has access to it
|
// Verify that the mission exists and user has access to it
|
||||||
|
console.log('Verifying mission access...');
|
||||||
const mission = await prisma.mission.findUnique({
|
const mission = await prisma.mission.findUnique({
|
||||||
where: { id: missionId },
|
where: { id: missionId },
|
||||||
select: { id: true, creatorId: true }
|
select: { id: true, creatorId: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!mission) {
|
if (!mission) {
|
||||||
|
console.log('Mission not found:', missionId);
|
||||||
return NextResponse.json({ error: 'Mission not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Mission not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently only allow creator to upload files
|
// Currently only allow creator to upload files
|
||||||
if (mission.creatorId !== userId) {
|
if (mission.creatorId !== userId) {
|
||||||
|
console.log('User not authorized to upload to this mission', { userId, creatorId: mission.creatorId });
|
||||||
return NextResponse.json({ error: 'Not authorized to upload to this mission' }, { status: 403 });
|
return NextResponse.json({ error: 'Not authorized to upload to this mission' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
console.log('Mission access verified');
|
||||||
|
|
||||||
if (type === 'logo') {
|
if (type === 'logo') {
|
||||||
// Upload logo file to Minio
|
console.log('Processing logo upload...');
|
||||||
const { filePath } = await uploadMissionLogo(userId, missionId, file);
|
try {
|
||||||
|
// Upload logo file to Minio
|
||||||
// Update mission record with logo path
|
const { filePath } = await uploadMissionLogo(userId, missionId, file);
|
||||||
await prisma.mission.update({
|
console.log('Logo uploaded successfully to path:', filePath);
|
||||||
where: { id: missionId },
|
|
||||||
data: { logo: filePath }
|
// Generate public URL
|
||||||
});
|
const publicUrl = getPublicUrl(filePath);
|
||||||
|
console.log('Public URL for logo:', publicUrl);
|
||||||
return NextResponse.json({ success: true, filePath });
|
|
||||||
|
// Update mission record with logo path
|
||||||
|
console.log('Updating mission record with logo path...');
|
||||||
|
await prisma.mission.update({
|
||||||
|
where: { id: missionId },
|
||||||
|
data: { logo: filePath }
|
||||||
|
});
|
||||||
|
console.log('Mission record updated');
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
filePath,
|
||||||
|
publicUrl
|
||||||
|
});
|
||||||
|
} catch (logoError) {
|
||||||
|
console.error('Error in logo upload process:', logoError);
|
||||||
|
return NextResponse.json({
|
||||||
|
error: 'Logo upload failed',
|
||||||
|
details: logoError instanceof Error ? logoError.message : String(logoError)
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (type === 'attachment') {
|
else if (type === 'attachment') {
|
||||||
// Upload attachment file to Minio
|
// Upload attachment file to Minio
|
||||||
const { filename, filePath, fileType, fileSize } = await uploadMissionAttachment(
|
console.log('Processing attachment upload...');
|
||||||
userId,
|
try {
|
||||||
missionId,
|
const { filename, filePath, fileType, fileSize } = await uploadMissionAttachment(
|
||||||
file
|
userId,
|
||||||
);
|
missionId,
|
||||||
|
file
|
||||||
// Create attachment record in database
|
);
|
||||||
const attachment = await prisma.attachment.create({
|
console.log('Attachment uploaded successfully to path:', filePath);
|
||||||
data: {
|
|
||||||
filename,
|
// Generate public URL
|
||||||
filePath,
|
const publicUrl = getPublicUrl(filePath);
|
||||||
fileType,
|
console.log('Public URL for attachment:', publicUrl);
|
||||||
fileSize,
|
|
||||||
missionId,
|
// Create attachment record in database
|
||||||
uploaderId: userId
|
console.log('Creating attachment record in database...');
|
||||||
}
|
const attachment = await prisma.attachment.create({
|
||||||
});
|
data: {
|
||||||
|
filename,
|
||||||
return NextResponse.json({
|
filePath,
|
||||||
success: true,
|
fileType,
|
||||||
attachment: {
|
fileSize,
|
||||||
id: attachment.id,
|
missionId,
|
||||||
filename: attachment.filename,
|
uploaderId: userId
|
||||||
filePath: attachment.filePath,
|
}
|
||||||
fileType: attachment.fileType,
|
});
|
||||||
fileSize: attachment.fileSize,
|
console.log('Attachment record created:', attachment.id);
|
||||||
createdAt: attachment.createdAt
|
|
||||||
}
|
return NextResponse.json({
|
||||||
});
|
success: true,
|
||||||
|
attachment: {
|
||||||
|
id: attachment.id,
|
||||||
|
filename: attachment.filename,
|
||||||
|
filePath: attachment.filePath,
|
||||||
|
publicUrl,
|
||||||
|
fileType: attachment.fileType,
|
||||||
|
fileSize: attachment.fileSize,
|
||||||
|
createdAt: attachment.createdAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (attachmentError) {
|
||||||
|
console.error('Error in attachment upload process:', attachmentError);
|
||||||
|
return NextResponse.json({
|
||||||
|
error: 'Attachment upload failed',
|
||||||
|
details: attachmentError instanceof Error ? attachmentError.message : String(attachmentError)
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
console.log('Invalid upload type:', type);
|
||||||
return NextResponse.json({ error: 'Invalid upload type' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid upload type' }, { status: 400 });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading file:', error);
|
console.error('Unhandled error in upload process:', error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
details: error instanceof Error ? error.message : String(error)
|
details: error instanceof Error ? error.message : String(error)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { getPublicUrl } from "@/lib/s3";
|
||||||
|
|
||||||
// Define Mission interface
|
// Define Mission interface
|
||||||
interface User {
|
interface User {
|
||||||
@ -198,11 +199,12 @@ export default function MissionsPage() {
|
|||||||
<div className="flex-shrink-0 mr-4 w-16 h-16 relative">
|
<div className="flex-shrink-0 mr-4 w-16 h-16 relative">
|
||||||
{mission.logo ? (
|
{mission.logo ? (
|
||||||
<img
|
<img
|
||||||
src={mission.logo}
|
src={mission.logo ? getPublicUrl(mission.logo) : ''}
|
||||||
alt={mission.name}
|
alt={mission.name}
|
||||||
className="w-full h-full object-cover rounded-md border border-gray-200"
|
className="w-full h-full object-cover rounded-md border border-gray-200"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.log("Logo failed to load:", mission.logo);
|
console.log("Logo failed to load:", mission.logo);
|
||||||
|
console.log("Full URL attempted:", mission.logo ? getPublicUrl(mission.logo) : 'undefined logo');
|
||||||
// If the image fails to load, show the fallback
|
// If the image fails to load, show the fallback
|
||||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||||
// Show the fallback div
|
// Show the fallback div
|
||||||
|
|||||||
@ -92,7 +92,23 @@ export function FileUpload({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!file || !session?.user?.id || !missionId) return;
|
if (!file || !session?.user?.id || !missionId) {
|
||||||
|
console.error('Upload failed: Missing required data', {
|
||||||
|
fileExists: !!file,
|
||||||
|
userIdExists: !!session?.user?.id,
|
||||||
|
missionIdExists: !!missionId
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting upload process...', {
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
fileType: file.type,
|
||||||
|
missionId,
|
||||||
|
userId: session.user.id,
|
||||||
|
uploadType: type
|
||||||
|
});
|
||||||
|
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
@ -104,18 +120,28 @@ export function FileUpload({
|
|||||||
formData.append('missionId', missionId);
|
formData.append('missionId', missionId);
|
||||||
formData.append('type', type);
|
formData.append('type', type);
|
||||||
|
|
||||||
|
console.log('FormData prepared, sending to API...');
|
||||||
|
|
||||||
// Upload the file
|
// Upload the file
|
||||||
const response = await fetch('/api/missions/upload', {
|
const response = await fetch('/api/missions/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('API response received:', {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
ok: response.ok
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(error.error || 'Upload failed');
|
console.error('API returned error:', errorData);
|
||||||
|
throw new Error(errorData.error || 'Upload failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
console.log('Upload successful, result:', result);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
|
|
||||||
// Reset file after successful upload
|
// Reset file after successful upload
|
||||||
@ -136,7 +162,7 @@ export function FileUpload({
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error details:', error);
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
toast({
|
toast({
|
||||||
title: 'Upload failed',
|
title: 'Upload failed',
|
||||||
|
|||||||
@ -22,17 +22,33 @@ export async function uploadMissionLogo(
|
|||||||
file: File
|
file: File
|
||||||
): Promise<{ filePath: string }> {
|
): Promise<{ filePath: string }> {
|
||||||
try {
|
try {
|
||||||
|
console.log('=== Starting logo upload process ===');
|
||||||
|
console.log('Upload params:', { userId, missionId, fileName: file.name, fileSize: file.size, fileType: file.type });
|
||||||
|
|
||||||
// Get file extension
|
// Get file extension
|
||||||
const fileExtension = file.name.substring(file.name.lastIndexOf('.'));
|
const fileExtension = file.name.substring(file.name.lastIndexOf('.'));
|
||||||
|
console.log('File extension:', fileExtension);
|
||||||
|
|
||||||
// Create file path
|
// Create file path
|
||||||
const filePath = getMissionLogoPath(userId, missionId, fileExtension);
|
const filePath = getMissionLogoPath(userId, missionId, fileExtension);
|
||||||
|
console.log('Generated file path:', filePath);
|
||||||
|
|
||||||
// Convert file to ArrayBuffer
|
// Convert file to ArrayBuffer
|
||||||
|
console.log('Converting file to buffer...');
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
console.log('Buffer created, size:', buffer.length);
|
||||||
|
|
||||||
// Upload to Minio
|
// Upload to Minio
|
||||||
|
console.log('Creating S3 command with bucket:', S3_CONFIG.bucket);
|
||||||
|
console.log('S3 config:', {
|
||||||
|
endpoint: S3_CONFIG.endpoint || 'MISSING!',
|
||||||
|
region: S3_CONFIG.region || 'MISSING!',
|
||||||
|
bucket: S3_CONFIG.bucket || 'MISSING!',
|
||||||
|
hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!',
|
||||||
|
hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!'
|
||||||
|
});
|
||||||
|
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: S3_CONFIG.bucket,
|
Bucket: S3_CONFIG.bucket,
|
||||||
Key: filePath,
|
Key: filePath,
|
||||||
@ -40,8 +56,16 @@ export async function uploadMissionLogo(
|
|||||||
ContentType: file.type,
|
ContentType: file.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
await s3Client.send(command);
|
console.log('Sending upload command to S3/Minio...');
|
||||||
|
try {
|
||||||
|
const result = await s3Client.send(command);
|
||||||
|
console.log('Upload successful, result:', result);
|
||||||
|
} catch (uploadError) {
|
||||||
|
console.error('S3 upload error details:', uploadError);
|
||||||
|
throw uploadError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Upload complete, returning file path:', filePath);
|
||||||
return { filePath };
|
return { filePath };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading mission logo:', error);
|
console.error('Error uploading mission logo:', error);
|
||||||
|
|||||||
119
lib/s3.ts
119
lib/s3.ts
@ -44,6 +44,27 @@ if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) {
|
|||||||
// Create S3 client
|
// Create S3 client
|
||||||
export const s3Client = new S3Client(s3Config);
|
export const s3Client = new S3Client(s3Config);
|
||||||
|
|
||||||
|
// Check Minio connection on startup
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
console.log('Testing Minio/S3 connection...');
|
||||||
|
// Try a simple operation to test the connection
|
||||||
|
const command = new ListObjectsV2Command({
|
||||||
|
Bucket: S3_CONFIG.bucket,
|
||||||
|
MaxKeys: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await s3Client.send(command);
|
||||||
|
console.log('Minio/S3 connection successful! Bucket exists and is accessible.');
|
||||||
|
console.log(`Bucket details: ${S3_CONFIG.bucket}, contains ${response.KeyCount || 0} objects`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CRITICAL ERROR: Failed to connect to Minio/S3 server!');
|
||||||
|
console.error('File uploads will fail until this is resolved.');
|
||||||
|
console.error('Error details:', error);
|
||||||
|
console.error('Please check your S3/Minio configuration and server status.');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Check for required environment variables
|
// Check for required environment variables
|
||||||
if (!S3_CONFIG.endpoint || !S3_CONFIG.bucket) {
|
if (!S3_CONFIG.endpoint || !S3_CONFIG.bucket) {
|
||||||
console.error('ERROR: Missing required S3 environment variables!');
|
console.error('ERROR: Missing required S3 environment variables!');
|
||||||
@ -119,8 +140,14 @@ export async function getObjectContent(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Put object (create or update a file)
|
// Put object (create or update a file)
|
||||||
export async function putObject(key: string, content: string, contentType?: string) {
|
export async function putObject(key: string, content: string | Buffer, contentType?: string) {
|
||||||
try {
|
try {
|
||||||
|
console.log(`Attempting to upload to S3/Minio: ${key}`);
|
||||||
|
|
||||||
|
if (!S3_CONFIG.bucket) {
|
||||||
|
throw new Error('S3 bucket name is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: S3_CONFIG.bucket,
|
Bucket: S3_CONFIG.bucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
@ -128,19 +155,25 @@ export async function putObject(key: string, content: string, contentType?: stri
|
|||||||
ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain')
|
ContentType: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`S3 PutObject request prepared for ${key}`);
|
||||||
const response = await s3Client.send(command);
|
const response = await s3Client.send(command);
|
||||||
|
console.log(`S3 PutObject successful for ${key}, ETag: ${response.ETag}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: key,
|
id: key,
|
||||||
title: key.split('/').pop()?.replace('.md', '') || '',
|
title: key.split('/').pop()?.replace('.md', '') || '',
|
||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
size: content.length,
|
size: typeof content === 'string' ? content.length : content.length,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
mime: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain'),
|
mime: contentType || (key.endsWith('.md') ? 'text/markdown' : 'text/plain'),
|
||||||
etag: response.ETag
|
etag: response.ETag
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error putting object:', error);
|
console.error(`Error putting object to S3/Minio (${key}):`, error);
|
||||||
|
// Check for specific S3 errors
|
||||||
|
if ((error as any)?.name === 'NoSuchBucket') {
|
||||||
|
console.error(`Bucket "${S3_CONFIG.bucket}" does not exist. Please create it first.`);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,4 +244,84 @@ export async function generatePresignedUrl(key: string, expiresIn = 3600) {
|
|||||||
console.error('Error generating presigned URL:', error);
|
console.error('Error generating presigned URL:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a public URL for a file stored in Minio/S3
|
||||||
|
export function getPublicUrl(filePath: string): string {
|
||||||
|
if (!filePath) return '';
|
||||||
|
if (filePath.startsWith('http')) return filePath; // Already a full URL
|
||||||
|
|
||||||
|
console.log('Generating public URL for:', filePath);
|
||||||
|
|
||||||
|
// Remove leading slash if present
|
||||||
|
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
||||||
|
|
||||||
|
// Construct the full URL
|
||||||
|
const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present
|
||||||
|
const bucket = S3_CONFIG.bucket;
|
||||||
|
|
||||||
|
console.log('S3 Config for URL generation:', {
|
||||||
|
endpoint,
|
||||||
|
bucket,
|
||||||
|
cleanPath
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return original path if no endpoint is configured
|
||||||
|
if (!endpoint) {
|
||||||
|
console.warn('No S3/Minio endpoint configured, returning original path');
|
||||||
|
return cleanPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct and return the full URL
|
||||||
|
const publicUrl = `${endpoint}/${bucket}/${cleanPath}`;
|
||||||
|
console.log('Generated public URL:', publicUrl);
|
||||||
|
return publicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Minio connection - can be called from browser console
|
||||||
|
export async function testMinioConnection() {
|
||||||
|
console.log('=== Testing Minio Connection ===');
|
||||||
|
try {
|
||||||
|
console.log('S3 Configuration:', {
|
||||||
|
endpoint: S3_CONFIG.endpoint || 'MISSING!',
|
||||||
|
region: S3_CONFIG.region || 'MISSING!',
|
||||||
|
bucket: S3_CONFIG.bucket || 'MISSING!',
|
||||||
|
hasAccessKey: !!S3_CONFIG.accessKey || 'MISSING!',
|
||||||
|
hasSecretKey: !!S3_CONFIG.secretKey || 'MISSING!',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try a simple operation
|
||||||
|
console.log('Attempting to list objects...');
|
||||||
|
const command = new ListObjectsV2Command({
|
||||||
|
Bucket: S3_CONFIG.bucket || '',
|
||||||
|
MaxKeys: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await s3Client.send(command);
|
||||||
|
console.log('Connection successful!');
|
||||||
|
console.log('Response:', response);
|
||||||
|
|
||||||
|
const files = response.Contents || [];
|
||||||
|
console.log(`Found ${files.length} files in bucket ${S3_CONFIG.bucket}:`);
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
console.log(` ${index + 1}. ${file.Key} (${file.Size} bytes)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Test completed successfully ===');
|
||||||
|
return { success: true, files: files.map(f => f.Key) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('=== Test failed ===');
|
||||||
|
console.error('Error details:', error);
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make testMinioConnection available globally if in browser
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).testMinioConnection = testMinioConnection;
|
||||||
|
|
||||||
|
// Also expose the getPublicUrl function
|
||||||
|
(window as any).getMinioUrl = getPublicUrl;
|
||||||
|
|
||||||
|
console.log('Minio test utilities available. Run window.testMinioConnection() to test Minio connection.');
|
||||||
}
|
}
|
||||||
160
scripts/test-minio-upload.js
Normal file
160
scripts/test-minio-upload.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* Test Minio Upload Script
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/test-minio-upload.js
|
||||||
|
*
|
||||||
|
* This script tests connectivity to Minio by uploading a simple test file
|
||||||
|
* and then trying to retrieve it. It uses the same S3 client configuration
|
||||||
|
* as the main application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
|
|
||||||
|
// Configuration for S3/Minio from environment variables
|
||||||
|
const S3_CONFIG = {
|
||||||
|
endpoint: process.env.MINIO_S3_UPLOAD_BUCKET_URL,
|
||||||
|
region: process.env.MINIO_AWS_REGION,
|
||||||
|
bucket: process.env.MINIO_AWS_S3_UPLOAD_BUCKET_NAME,
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY || process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretKey: process.env.MINIO_SECRET_KEY || process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize S3 client with standard configuration
|
||||||
|
const s3Config = {
|
||||||
|
region: S3_CONFIG.region,
|
||||||
|
endpoint: S3_CONFIG.endpoint,
|
||||||
|
forcePathStyle: true, // Required for MinIO
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add credentials if available
|
||||||
|
if (S3_CONFIG.accessKey && S3_CONFIG.secretKey) {
|
||||||
|
Object.assign(s3Config, {
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: S3_CONFIG.accessKey,
|
||||||
|
secretAccessKey: S3_CONFIG.secretKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create S3 client
|
||||||
|
const s3Client = new S3Client(s3Config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a public URL for a file stored in Minio/S3
|
||||||
|
*/
|
||||||
|
function getPublicUrl(filePath) {
|
||||||
|
if (!filePath) return '';
|
||||||
|
if (filePath.startsWith('http')) return filePath; // Already a full URL
|
||||||
|
|
||||||
|
// Remove leading slash if present
|
||||||
|
const cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
||||||
|
|
||||||
|
// Construct the full URL
|
||||||
|
const endpoint = S3_CONFIG.endpoint?.replace(/\/$/, ''); // Remove trailing slash if present
|
||||||
|
const bucket = S3_CONFIG.bucket;
|
||||||
|
|
||||||
|
// Return original path if no endpoint is configured
|
||||||
|
if (!endpoint) {
|
||||||
|
console.warn('No S3/Minio endpoint configured, returning original path');
|
||||||
|
return cleanPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct and return the full URL
|
||||||
|
return `${endpoint}/${bucket}/${cleanPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Minio connection by uploading and retrieving a test file
|
||||||
|
*/
|
||||||
|
async function testMinioUpload() {
|
||||||
|
// Generate a unique test file path
|
||||||
|
const testKey = `test/upload-test-${Date.now()}.txt`;
|
||||||
|
const testContent = 'This is a test file created at ' + new Date().toISOString();
|
||||||
|
|
||||||
|
console.log('=== Testing Minio Upload ===');
|
||||||
|
console.log('S3 Configuration:');
|
||||||
|
console.log(' Endpoint:', S3_CONFIG.endpoint || 'MISSING!');
|
||||||
|
console.log(' Region:', S3_CONFIG.region || 'MISSING!');
|
||||||
|
console.log(' Bucket:', S3_CONFIG.bucket || 'MISSING!');
|
||||||
|
console.log(' Has Access Key:', !!S3_CONFIG.accessKey || 'MISSING!');
|
||||||
|
console.log(' Has Secret Key:', !!S3_CONFIG.secretKey || 'MISSING!');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Upload test file
|
||||||
|
console.log('\nStep 1: Uploading test file...');
|
||||||
|
console.log(' File path:', testKey);
|
||||||
|
console.log(' Content:', testContent);
|
||||||
|
|
||||||
|
const uploadCommand = new PutObjectCommand({
|
||||||
|
Bucket: S3_CONFIG.bucket,
|
||||||
|
Key: testKey,
|
||||||
|
Body: testContent,
|
||||||
|
ContentType: 'text/plain'
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadResult = await s3Client.send(uploadCommand);
|
||||||
|
console.log(' Upload successful!');
|
||||||
|
console.log(' ETag:', uploadResult.ETag);
|
||||||
|
|
||||||
|
// 2. Generate public URL
|
||||||
|
const publicUrl = getPublicUrl(testKey);
|
||||||
|
console.log('\nStep 2: Generated public URL:');
|
||||||
|
console.log(' URL:', publicUrl);
|
||||||
|
|
||||||
|
// 3. Download the file to verify it was uploaded correctly
|
||||||
|
console.log('\nStep 3: Downloading test file to verify...');
|
||||||
|
const downloadCommand = new GetObjectCommand({
|
||||||
|
Bucket: S3_CONFIG.bucket,
|
||||||
|
Key: testKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadResult = await s3Client.send(downloadCommand);
|
||||||
|
const downloadedContent = await downloadResult.Body.transformToString();
|
||||||
|
|
||||||
|
console.log(' Download successful!');
|
||||||
|
console.log(' Content:', downloadedContent);
|
||||||
|
|
||||||
|
if (downloadedContent === testContent) {
|
||||||
|
console.log(' Content verification: ✓ Matches original');
|
||||||
|
} else {
|
||||||
|
console.log(' Content verification: ✗ Does not match original');
|
||||||
|
console.log(' Expected:', testContent);
|
||||||
|
console.log(' Received:', downloadedContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cleanup - Delete the test file
|
||||||
|
console.log('\nStep 4: Cleaning up - deleting test file...');
|
||||||
|
const deleteCommand = new DeleteObjectCommand({
|
||||||
|
Bucket: S3_CONFIG.bucket,
|
||||||
|
Key: testKey
|
||||||
|
});
|
||||||
|
|
||||||
|
await s3Client.send(deleteCommand);
|
||||||
|
console.log(' Deletion successful!');
|
||||||
|
|
||||||
|
console.log('\n=== Test completed successfully ===');
|
||||||
|
console.log('Minio is properly configured and accessible.');
|
||||||
|
console.log('File uploads should be working correctly.');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n=== Test failed ===');
|
||||||
|
console.error('Error details:', error);
|
||||||
|
console.error('\nPossible issues:');
|
||||||
|
console.error('1. Minio server is not running or not accessible');
|
||||||
|
console.error('2. Incorrect endpoint URL');
|
||||||
|
console.error('3. Invalid credentials');
|
||||||
|
console.error('4. Bucket does not exist or is not accessible');
|
||||||
|
console.error('5. Network connectivity issues');
|
||||||
|
|
||||||
|
console.error('\nEnvironment variables to check:');
|
||||||
|
console.error('- MINIO_S3_UPLOAD_BUCKET_URL');
|
||||||
|
console.error('- MINIO_AWS_S3_UPLOAD_BUCKET_NAME');
|
||||||
|
console.error('- MINIO_ACCESS_KEY / AWS_ACCESS_KEY_ID');
|
||||||
|
console.error('- MINIO_SECRET_KEY / AWS_SECRET_ACCESS_KEY');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testMinioUpload();
|
||||||
Loading…
Reference in New Issue
Block a user