Merge pull request 'upload-issue' (#9) from upload-issue into main
Reviewed-on: #9
This commit is contained in:
commit
6e9f37c1cd
File diff suppressed because one or more lines are too long
@ -142,6 +142,7 @@ WantedBy=multi-user.target
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name forensic-pathways.yourdomain.com;
|
server_name forensic-pathways.yourdomain.com;
|
||||||
|
client_max_body_size 50M; # Important for uploads
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://localhost:4321;
|
proxy_pass http://localhost:4321;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// src/pages/api/upload/media.ts
|
// src/pages/api/upload/media.ts - Enhanced with detailed logging and error handling
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { withAPIAuth } from '../../../utils/auth.js';
|
import { withAPIAuth } from '../../../utils/auth.js';
|
||||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||||
@ -21,8 +21,13 @@ interface UploadResult {
|
|||||||
const UPLOAD_CONFIG = {
|
const UPLOAD_CONFIG = {
|
||||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||||
allowedTypes: new Set([
|
allowedTypes: new Set([
|
||||||
|
// Images
|
||||||
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||||
|
|
||||||
|
// Videos
|
||||||
'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
|
'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
|
||||||
|
|
||||||
|
// Documents
|
||||||
'application/pdf',
|
'application/pdf',
|
||||||
'application/msword',
|
'application/msword',
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
@ -30,7 +35,32 @@ const UPLOAD_CONFIG = {
|
|||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
'application/vnd.ms-powerpoint',
|
'application/vnd.ms-powerpoint',
|
||||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
'text/plain', 'text/csv', 'application/json'
|
|
||||||
|
// Text files
|
||||||
|
'text/plain',
|
||||||
|
'text/csv',
|
||||||
|
'text/markdown', // Added markdown
|
||||||
|
'text/x-markdown', // Alternative markdown MIME type
|
||||||
|
'application/json',
|
||||||
|
'application/xml',
|
||||||
|
'text/xml',
|
||||||
|
'text/html',
|
||||||
|
|
||||||
|
// Archives
|
||||||
|
'application/zip',
|
||||||
|
'application/x-tar',
|
||||||
|
'application/gzip',
|
||||||
|
'application/x-gzip',
|
||||||
|
'application/x-zip-compressed',
|
||||||
|
'application/x-rar-compressed',
|
||||||
|
'application/x-7z-compressed',
|
||||||
|
|
||||||
|
// Additional useful formats
|
||||||
|
'application/rtf', // Rich Text Format
|
||||||
|
'text/richtext',
|
||||||
|
'application/x-yaml', // YAML files
|
||||||
|
'text/yaml',
|
||||||
|
'application/yaml'
|
||||||
]),
|
]),
|
||||||
localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
|
localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
|
||||||
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
|
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
|
||||||
@ -50,6 +80,7 @@ function checkUploadRateLimit(userEmail: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userLimit.count >= RATE_LIMIT_MAX) {
|
if (userLimit.count >= RATE_LIMIT_MAX) {
|
||||||
|
console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,27 +89,37 @@ function checkUploadRateLimit(userEmail: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateFile(file: File): { valid: boolean; error?: string } {
|
function validateFile(file: File): { valid: boolean; error?: string } {
|
||||||
|
console.log(`[UPLOAD] Validating file: ${file.name}, size: ${file.size}, type: ${file.type}`);
|
||||||
|
|
||||||
if (file.size > UPLOAD_CONFIG.maxFileSize) {
|
if (file.size > UPLOAD_CONFIG.maxFileSize) {
|
||||||
return {
|
const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`;
|
||||||
valid: false,
|
console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`);
|
||||||
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`
|
return { valid: false, error: errorMsg };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
|
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
|
||||||
return {
|
const errorMsg = `File type ${file.type} not allowed`;
|
||||||
valid: false,
|
console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
|
||||||
error: `File type ${file.type} not allowed`
|
return { valid: false, error: errorMsg };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[UPLOAD] File validation passed for: ${file.name}`);
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
|
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
|
||||||
|
console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploader = new NextcloudUploader();
|
const uploader = new NextcloudUploader();
|
||||||
const result = await uploader.uploadFile(file, userEmail);
|
const result = await uploader.uploadFile(file, userEmail);
|
||||||
|
|
||||||
|
console.log(`[UPLOAD] Nextcloud upload successful:`, {
|
||||||
|
filename: result.filename,
|
||||||
|
url: result.url,
|
||||||
|
size: file.size
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
url: result.url,
|
url: result.url,
|
||||||
@ -87,7 +128,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
|
|||||||
storage: 'nextcloud'
|
storage: 'nextcloud'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Nextcloud upload failed:', error);
|
console.error('[UPLOAD] Nextcloud upload failed:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
|
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
|
||||||
@ -97,7 +138,10 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
|
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
|
||||||
|
console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`);
|
||||||
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
@ -106,11 +150,20 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
|||||||
const filename = `${timestamp}-${randomString}${extension}`;
|
const filename = `${timestamp}-${randomString}${extension}`;
|
||||||
|
|
||||||
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
|
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
|
||||||
|
console.log(`[UPLOAD] Writing file to: ${filepath}`);
|
||||||
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
await fs.writeFile(filepath, buffer);
|
await fs.writeFile(filepath, buffer);
|
||||||
|
|
||||||
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
|
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
|
||||||
|
|
||||||
|
console.log(`[UPLOAD] Local upload successful:`, {
|
||||||
|
filename,
|
||||||
|
filepath,
|
||||||
|
publicUrl,
|
||||||
|
size: file.size
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
url: publicUrl,
|
url: publicUrl,
|
||||||
@ -119,7 +172,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
|||||||
storage: 'local'
|
storage: 'local'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Local upload failed:', error);
|
console.error('[UPLOAD] Local upload failed:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Local upload failed',
|
error: error instanceof Error ? error.message : 'Local upload failed',
|
||||||
@ -130,12 +183,23 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
|
|||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
return await handleAPIRequest(async () => {
|
return await handleAPIRequest(async () => {
|
||||||
|
console.log('[UPLOAD] Processing upload request');
|
||||||
|
|
||||||
|
// Enhanced auth logging
|
||||||
const authResult = await withAPIAuth(request, 'contributions');
|
const authResult = await withAPIAuth(request, 'contributions');
|
||||||
|
console.log('[UPLOAD] Auth result:', {
|
||||||
|
authenticated: authResult.authenticated,
|
||||||
|
authRequired: authResult.authRequired,
|
||||||
|
userId: authResult.userId
|
||||||
|
});
|
||||||
|
|
||||||
if (authResult.authRequired && !authResult.authenticated) {
|
if (authResult.authRequired && !authResult.authenticated) {
|
||||||
return apiError.unauthorized();
|
console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated');
|
||||||
|
return apiError.unauthorized('Authentication required for file uploads');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userEmail = authResult.session?.email || 'anon@anon.anon';
|
const userEmail = authResult.session?.email || 'anon@anon.anon';
|
||||||
|
console.log(`[UPLOAD] Processing upload for user: ${userEmail}`);
|
||||||
|
|
||||||
if (!checkUploadRateLimit(userEmail)) {
|
if (!checkUploadRateLimit(userEmail)) {
|
||||||
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
|
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
|
||||||
@ -143,38 +207,60 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
let formData;
|
let formData;
|
||||||
try {
|
try {
|
||||||
|
console.log('[UPLOAD] Parsing form data');
|
||||||
formData = await request.formData();
|
formData = await request.formData();
|
||||||
|
console.log('[UPLOAD] Form data keys:', Array.from(formData.keys()));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return apiError.badRequest('Invalid form data');
|
console.error('[UPLOAD] Failed to parse form data:', error);
|
||||||
|
return apiError.badRequest('Invalid form data - could not parse request');
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = formData.get('file') as File;
|
const file = formData.get('file') as File;
|
||||||
const type = formData.get('type') as string;
|
const type = formData.get('type') as string;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
console.warn('[UPLOAD] No file provided in request');
|
||||||
return apiSpecial.missingRequired(['file']);
|
return apiSpecial.missingRequired(['file']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`);
|
||||||
|
|
||||||
const validation = validateFile(file);
|
const validation = validateFile(file);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return apiError.badRequest(validation.error!);
|
return apiError.badRequest(validation.error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced environment logging
|
||||||
|
const nextcloudConfigured = isNextcloudConfigured();
|
||||||
|
console.log('[UPLOAD] Environment check:', {
|
||||||
|
nextcloudConfigured,
|
||||||
|
localUploadPath: UPLOAD_CONFIG.localUploadPath,
|
||||||
|
publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl,
|
||||||
|
nodeEnv: process.env.NODE_ENV
|
||||||
|
});
|
||||||
|
|
||||||
let result: UploadResult;
|
let result: UploadResult;
|
||||||
|
|
||||||
if (isNextcloudConfigured()) {
|
if (nextcloudConfigured) {
|
||||||
|
console.log('[UPLOAD] Using Nextcloud as primary storage');
|
||||||
result = await uploadToNextcloud(file, userEmail);
|
result = await uploadToNextcloud(file, userEmail);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
|
console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error);
|
||||||
result = await uploadToLocal(file, type);
|
result = await uploadToLocal(file, type);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[UPLOAD] Using local storage (Nextcloud not configured)');
|
||||||
result = await uploadToLocal(file, type);
|
result = await uploadToLocal(file, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
|
console.log(`[UPLOAD] Upload completed successfully:`, {
|
||||||
|
filename: result.filename,
|
||||||
|
storage: result.storage,
|
||||||
|
url: result.url,
|
||||||
|
user: userEmail
|
||||||
|
});
|
||||||
|
|
||||||
return apiSpecial.uploadSuccess({
|
return apiSpecial.uploadSuccess({
|
||||||
url: result.url!,
|
url: result.url!,
|
||||||
@ -183,7 +269,12 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
storage: result.storage!
|
storage: result.storage!
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
|
console.error(`[UPLOAD] Upload failed completely:`, {
|
||||||
|
filename: file.name,
|
||||||
|
error: result.error,
|
||||||
|
storage: result.storage,
|
||||||
|
user: userEmail
|
||||||
|
});
|
||||||
|
|
||||||
return apiSpecial.uploadFailed(result.error!);
|
return apiSpecial.uploadFailed(result.error!);
|
||||||
}
|
}
|
||||||
@ -193,6 +284,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
return await handleAPIRequest(async () => {
|
return await handleAPIRequest(async () => {
|
||||||
|
console.log('[UPLOAD] Getting upload status');
|
||||||
|
|
||||||
const authResult = await withAPIAuth(request);
|
const authResult = await withAPIAuth(request);
|
||||||
if (authResult.authRequired && !authResult.authenticated) {
|
if (authResult.authRequired && !authResult.authenticated) {
|
||||||
return apiError.unauthorized();
|
return apiError.unauthorized();
|
||||||
@ -204,12 +297,14 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
try {
|
try {
|
||||||
await fs.access(UPLOAD_CONFIG.localUploadPath);
|
await fs.access(UPLOAD_CONFIG.localUploadPath);
|
||||||
localStorageAvailable = true;
|
localStorageAvailable = true;
|
||||||
|
console.log('[UPLOAD] Local storage accessible');
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
|
||||||
localStorageAvailable = true;
|
localStorageAvailable = true;
|
||||||
|
console.log('[UPLOAD] Local storage created');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Local upload directory not accessible:', error);
|
console.warn('[UPLOAD] Local upload directory not accessible:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,9 +332,14 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
paths: {
|
paths: {
|
||||||
uploadEndpoint: '/api/upload/media',
|
uploadEndpoint: '/api/upload/media',
|
||||||
localPath: localStorageAvailable ? '/uploads' : null
|
localPath: localStorageAvailable ? '/uploads' : null
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
nodeEnv: process.env.NODE_ENV,
|
||||||
|
publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[UPLOAD] Status check completed:', status);
|
||||||
return apiResponse.success(status);
|
return apiResponse.success(status);
|
||||||
|
|
||||||
}, 'Upload status retrieval failed');
|
}, 'Upload status retrieval failed');
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
|
// src/pages/contribute/knowledgebase.astro
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { withAuth } from '../../utils/auth.js';
|
import { withAuth } from '../../utils/auth.js';
|
||||||
import { getToolsData } from '../../utils/dataService.js';
|
import { getToolsData } from '../../utils/dataService.js';
|
||||||
@ -114,8 +114,13 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
|
<label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
|
||||||
<div class="upload-area" id="upload-area">
|
<div class="upload-area" id="upload-area">
|
||||||
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" class="hidden">
|
<input
|
||||||
<div class="upload-placeholder">
|
type="file"
|
||||||
|
id="file-input"
|
||||||
|
multiple
|
||||||
|
accept=".pdf,.doc,.docx,.txt,.md,.markdown,.csv,.json,.xml,.html,.rtf,.yaml,.yml,.zip,.tar,.gz,.rar,.7z,.png,.jpg,.jpeg,.gif,.webp,.svg,.mp4,.webm,.mov,.avi"
|
||||||
|
class="hidden"
|
||||||
|
> <div class="upload-placeholder">
|
||||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||||
<polyline points="7 10 12 15 17 10"/>
|
<polyline points="7 10 12 15 17 10"/>
|
||||||
@ -304,6 +309,13 @@ class KnowledgebaseForm {
|
|||||||
|
|
||||||
private handleFiles(files: File[]) {
|
private handleFiles(files: File[]) {
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
|
// Client-side validation before upload
|
||||||
|
const validation = this.validateFileBeforeUpload(file);
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.log('[UPLOAD]Cannot upload ', file.name, ' Error: ', validation.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||||
const newFile: UploadedFile = {
|
const newFile: UploadedFile = {
|
||||||
id: fileId,
|
id: fileId,
|
||||||
@ -317,30 +329,99 @@ class KnowledgebaseForm {
|
|||||||
this.renderFileList();
|
this.renderFileList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateFileBeforeUpload(file: File): { valid: boolean; error?: string } {
|
||||||
|
const maxSizeBytes = 50 * 1024 * 1024; // 50MB
|
||||||
|
if (file.size > maxSizeBytes) {
|
||||||
|
const sizeMB = (file.size / 1024 / 1024).toFixed(1);
|
||||||
|
const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(0);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `File too large (${sizeMB}MB). Maximum size: ${maxMB}MB`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file type
|
||||||
|
const allowedExtensions = [
|
||||||
|
'.pdf', '.doc', '.docx', '.txt', '.md', '.markdown', '.csv', '.json',
|
||||||
|
'.xml', '.html', '.rtf', '.yaml', '.yml', '.zip', '.tar', '.gz',
|
||||||
|
'.rar', '.7z', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg',
|
||||||
|
'.mp4', '.webm', '.mov', '.avi'
|
||||||
|
];
|
||||||
|
|
||||||
|
const fileName = file.name.toLowerCase();
|
||||||
|
const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
|
||||||
|
|
||||||
|
if (!hasValidExtension) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `File type not allowed. Allowed: ${allowedExtensions.join(', ')}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
private async uploadFile(fileId: string) {
|
private async uploadFile(fileId: string) {
|
||||||
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
|
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
|
||||||
if (!fileItem) return;
|
if (!fileItem) {
|
||||||
|
console.error('[UPLOAD] File item not found for ID:', fileId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[UPLOAD] Starting upload for:', fileItem.name, 'Size:', fileItem.file.size, 'Type:', fileItem.file.type);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', fileItem.file);
|
formData.append('file', fileItem.file);
|
||||||
formData.append('type', 'knowledgebase');
|
formData.append('type', 'knowledgebase');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('[UPLOAD] Sending request to /api/upload/media');
|
||||||
|
|
||||||
const response = await fetch('/api/upload/media', {
|
const response = await fetch('/api/upload/media', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[UPLOAD] Response status:', response.status);
|
||||||
|
|
||||||
|
let responseText: string;
|
||||||
|
let responseData: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseText = await response.text();
|
||||||
|
console.log('[UPLOAD] Raw response:', responseText.substring(0, 200));
|
||||||
|
|
||||||
|
try {
|
||||||
|
responseData = JSON.parse(responseText);
|
||||||
|
} catch (parseError) {
|
||||||
|
responseData = { error: responseText };
|
||||||
|
}
|
||||||
|
} catch (readError) {
|
||||||
|
console.error('[UPLOAD] Failed to read response:', readError);
|
||||||
|
throw new Error('Failed to read server response');
|
||||||
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
console.log('[UPLOAD] Success result:', responseData);
|
||||||
|
|
||||||
fileItem.uploaded = true;
|
fileItem.uploaded = true;
|
||||||
fileItem.url = result.url;
|
fileItem.url = responseData.url;
|
||||||
this.renderFileList();
|
this.renderFileList();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Upload failed');
|
|
||||||
|
if (responseData && responseData.details) {
|
||||||
|
console.error('[UPLOAD] Error details:', responseData.details);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showMessage('error', `Failed to upload ${fileItem.name}`);
|
console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown upload error';
|
||||||
|
|
||||||
this.removeFile(fileId);
|
this.removeFile(fileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,7 +493,6 @@ class KnowledgebaseForm {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[KB FORM] Submission error:', error);
|
console.error('[KB FORM] Submission error:', error);
|
||||||
this.showMessage('error', 'Submission failed. Please try again.');
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
(this.elements.submitBtn as HTMLButtonElement).disabled = false;
|
(this.elements.submitBtn as HTMLButtonElement).disabled = false;
|
||||||
@ -441,18 +521,6 @@ class KnowledgebaseForm {
|
|||||||
this.renderFileList();
|
this.renderFileList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private showMessage(type: 'success' | 'error' | 'warning', message: string) {
|
|
||||||
const container = document.getElementById('message-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const messageEl = document.createElement('div');
|
|
||||||
messageEl.className = `message message-${type}`;
|
|
||||||
messageEl.textContent = message;
|
|
||||||
|
|
||||||
container.appendChild(messageEl);
|
|
||||||
setTimeout(() => messageEl.remove(), 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeFileById(fileId: string) {
|
public removeFileById(fileId: string) {
|
||||||
this.removeFile(fileId);
|
this.removeFile(fileId);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user