api unification
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// src/pages/api/upload/media.ts
|
||||
// src/pages/api/upload/media.ts (UPDATED - Using consolidated API responses)
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getSessionFromRequest, verifySession, withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
import { NextcloudUploader, isNextcloudConfigured } from '../../../utils/nextcloud.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
@@ -62,102 +63,38 @@ function checkUploadRateLimit(userEmail: string): boolean {
|
||||
}
|
||||
|
||||
function validateFile(file: File): { valid: boolean; error?: string } {
|
||||
// Check file size
|
||||
// File size check
|
||||
if (file.size > UPLOAD_CONFIG.maxFileSize) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `File too large (max ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB)`
|
||||
return {
|
||||
valid: false,
|
||||
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`
|
||||
};
|
||||
}
|
||||
|
||||
// Check file type
|
||||
// File type check
|
||||
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `File type not allowed: ${file.type}`
|
||||
};
|
||||
}
|
||||
|
||||
// Check filename
|
||||
if (!file.name || file.name.trim().length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid filename'
|
||||
return {
|
||||
valid: false,
|
||||
error: `File type ${file.type} not allowed`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function sanitizeFilename(filename: string): string {
|
||||
// Remove or replace unsafe characters
|
||||
return filename
|
||||
.replace(/[^a-zA-Z0-9._-]/g, '_') // Replace unsafe chars with underscore
|
||||
.replace(/_{2,}/g, '_') // Replace multiple underscores with single
|
||||
.replace(/^_|_$/g, '') // Remove leading/trailing underscores
|
||||
.toLowerCase()
|
||||
.substring(0, 100); // Limit length
|
||||
}
|
||||
|
||||
function generateUniqueFilename(originalName: string): string {
|
||||
const timestamp = Date.now();
|
||||
const randomId = crypto.randomBytes(4).toString('hex');
|
||||
const ext = path.extname(originalName);
|
||||
const base = path.basename(originalName, ext);
|
||||
const sanitizedBase = sanitizeFilename(base);
|
||||
|
||||
return `${timestamp}_${randomId}_${sanitizedBase}${ext}`;
|
||||
}
|
||||
|
||||
async function uploadToLocal(file: File, category: string): Promise<UploadResult> {
|
||||
try {
|
||||
// Ensure upload directory exists
|
||||
const categoryDir = path.join(UPLOAD_CONFIG.localUploadPath, sanitizeFilename(category));
|
||||
await fs.mkdir(categoryDir, { recursive: true });
|
||||
|
||||
// Generate unique filename
|
||||
const uniqueFilename = generateUniqueFilename(file.name);
|
||||
const filePath = path.join(categoryDir, uniqueFilename);
|
||||
|
||||
// Convert file to buffer and write
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
// Generate public URL
|
||||
const relativePath = path.posix.join('/uploads', sanitizeFilename(category), uniqueFilename);
|
||||
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}${relativePath}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
filename: uniqueFilename,
|
||||
size: file.size,
|
||||
storage: 'local'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Local upload error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Local upload failed',
|
||||
storage: 'local'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadToNextcloud(file: File, category: string): Promise<UploadResult> {
|
||||
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
|
||||
try {
|
||||
const uploader = new NextcloudUploader();
|
||||
const result = await uploader.uploadFile(file, category);
|
||||
|
||||
const result = await uploader.uploadFile(file, userEmail);
|
||||
return {
|
||||
...result,
|
||||
success: true,
|
||||
url: result.url,
|
||||
filename: result.filename,
|
||||
size: file.size,
|
||||
storage: 'nextcloud'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Nextcloud upload error:', error);
|
||||
console.error('Nextcloud upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
|
||||
@@ -166,66 +103,91 @@ async function uploadToNextcloud(file: File, category: string): Promise<UploadRe
|
||||
}
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
|
||||
try {
|
||||
// Check authentication
|
||||
// Ensure upload directory exists
|
||||
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
||||
|
||||
// Generate unique filename
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const randomString = crypto.randomBytes(8).toString('hex');
|
||||
const extension = path.extname(file.name);
|
||||
const filename = `${timestamp}-${randomString}${extension}`;
|
||||
|
||||
// Save file
|
||||
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
await fs.writeFile(filepath, buffer);
|
||||
|
||||
// Generate public URL
|
||||
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
filename: filename,
|
||||
size: file.size,
|
||||
storage: 'local'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Local upload failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Local upload failed',
|
||||
storage: 'local'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// POST endpoint for file uploads
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
// Authentication check
|
||||
const authResult = await withAPIAuth(request);
|
||||
if (authResult.authRequired && !authResult.authenticated) {
|
||||
return createAuthErrorResponse('Authentication required');
|
||||
return apiError.unauthorized();
|
||||
}
|
||||
|
||||
const userEmail = authResult.session?.email || 'anonymous';
|
||||
const userEmail = authResult.session?.email || 'anonymous@example.com';
|
||||
|
||||
|
||||
// Rate limiting
|
||||
if (!checkUploadRateLimit(userEmail)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Upload rate limit exceeded. Please wait before uploading more files.'
|
||||
}), {
|
||||
status: 429,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
|
||||
}
|
||||
|
||||
// Parse form data
|
||||
const formData = await request.formData();
|
||||
|
||||
// Parse multipart form data
|
||||
let formData;
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch (error) {
|
||||
return apiError.badRequest('Invalid form data');
|
||||
}
|
||||
|
||||
const file = formData.get('file') as File;
|
||||
const type = formData.get('type') as string || 'general';
|
||||
|
||||
const type = formData.get('type') as string;
|
||||
|
||||
if (!file) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'No file provided'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return apiSpecial.missingRequired(['file']);
|
||||
}
|
||||
|
||||
|
||||
// Validate file
|
||||
const validation = validateFile(file);
|
||||
if (!validation.valid) {
|
||||
return new Response(JSON.stringify({
|
||||
error: validation.error
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return apiError.badRequest(validation.error!);
|
||||
}
|
||||
|
||||
// Determine upload strategy
|
||||
const useNextcloud = isNextcloudConfigured();
|
||||
|
||||
// Attempt upload (Nextcloud first, then local fallback)
|
||||
let result: UploadResult;
|
||||
|
||||
if (useNextcloud) {
|
||||
// Try Nextcloud first, fallback to local
|
||||
result = await uploadToNextcloud(file, type);
|
||||
if (isNextcloudConfigured()) {
|
||||
result = await uploadToNextcloud(file, userEmail);
|
||||
|
||||
// If Nextcloud fails, try local fallback
|
||||
if (!result.success) {
|
||||
console.warn('Nextcloud upload failed, falling back to local storage:', result.error);
|
||||
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
|
||||
result = await uploadToLocal(file, type);
|
||||
}
|
||||
} else {
|
||||
// Use local storage
|
||||
result = await uploadToLocal(file, type);
|
||||
}
|
||||
|
||||
@@ -233,46 +195,48 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
// Log successful upload
|
||||
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
// BEFORE: Manual success response (5 lines)
|
||||
// return new Response(JSON.stringify(result), {
|
||||
// status: 200,
|
||||
// headers: { 'Content-Type': 'application/json' }
|
||||
// });
|
||||
|
||||
// AFTER: Single line with specialized helper
|
||||
return apiSpecial.uploadSuccess({
|
||||
url: result.url!,
|
||||
filename: result.filename!,
|
||||
size: result.size!,
|
||||
storage: result.storage!
|
||||
});
|
||||
} else {
|
||||
// Log failed upload
|
||||
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
// BEFORE: Manual error response (5 lines)
|
||||
// return new Response(JSON.stringify(result), {
|
||||
// status: 500,
|
||||
// headers: { 'Content-Type': 'application/json' }
|
||||
// });
|
||||
|
||||
// AFTER: Single line with specialized helper
|
||||
return apiSpecial.uploadFailed(result.error!);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Media upload API error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}, 'Media upload processing failed');
|
||||
};
|
||||
|
||||
// GET endpoint for upload status/info
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
// Check authentication
|
||||
return await handleAPIRequest(async () => {
|
||||
// Authentication check
|
||||
const authResult = await withAPIAuth(request);
|
||||
if (authResult.authRequired && !authResult.authenticated) {
|
||||
return createAuthErrorResponse('Authentication required');
|
||||
return apiError.unauthorized();
|
||||
}
|
||||
|
||||
// Return upload configuration and status
|
||||
const nextcloudConfigured = isNextcloudConfigured();
|
||||
|
||||
|
||||
// Check local upload directory
|
||||
let localStorageAvailable = false;
|
||||
try {
|
||||
@@ -314,19 +278,7 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(status), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return apiResponse.success(status);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Media upload status error:', error);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Failed to get upload status'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}, 'Upload status retrieval failed');
|
||||
};
|
||||
Reference in New Issue
Block a user