api unification

This commit is contained in:
overcuriousity
2025-07-24 20:13:11 +02:00
parent f76999ed2e
commit 209f173d7a
7 changed files with 408 additions and 617 deletions

View File

@@ -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');
};