/** * DocumentUploadComponent.jsx * * React component for uploading documents to the SCM document storage service. * Handles complete workflow: initialize → upload → complete → download */ import React, { useState, useRef } from 'react'; const DocumentUploadComponent = ({ token, domain, entityId, category }) => { const [uploadStatus, setUploadStatus] = useState('idle'); // idle, uploading, complete, error const [uploadProgress, setUploadProgress] = useState(0); const [objectId, setObjectId] = useState(null); const [error, setError] = useState(null); const [metadata, setMetadata] = useState(null); const fileInputRef = useRef(null); const API_BASE = 'http://localhost:8000'; /** * Calculate SHA-256 checksum of file */ const calculateChecksum = async (file) => { const buffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); }; /** * Step 1: Initialize upload */ const initializeUpload = async (file) => { try { const response = await fetch(`${API_BASE}/scm/storage/upload/init`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ domain, entity_id: entityId, category, file_name: file.name, mime_type: file.type, file_size: file.size, visibility: 'private', }), }); if (!response.ok) { const errorData = await response.json(); throw new Error( `Upload init failed: ${response.status} - ${errorData.detail || response.statusText}` ); } return await response.json(); } catch (err) { throw new Error(`Initialize upload failed: ${err.message}`); } }; /** * Step 2: Upload file to MinIO presigned URL */ const uploadToPresignedUrl = async (file, presignedUrl) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); // Track upload progress xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; setUploadProgress(Math.round(percentComplete)); } }); xhr.addEventListener('load', () => { if (xhr.status === 200) { resolve(); } else { reject( new Error( `Upload to MinIO failed: ${xhr.status} - ${xhr.statusText}` ) ); } }); xhr.addEventListener('error', () => { reject(new Error('Upload to MinIO failed: Network error')); }); xhr.addEventListener('abort', () => { reject(new Error('Upload to MinIO cancelled')); }); xhr.open('PUT', presignedUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.send(file); }); }; /** * Step 3: Complete upload */ const completeUpload = async (uploadId, checksumSha256) => { try { const response = await fetch(`${API_BASE}/scm/storage/upload/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ upload_id: uploadId, checksum_sha256: checksumSha256, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error( `Upload complete failed: ${response.status} - ${errorData.detail || response.statusText}` ); } return await response.json(); } catch (err) { throw new Error(`Complete upload failed: ${err.message}`); } }; /** * Main upload handler */ const handleUpload = async (file) => { try { setUploadStatus('uploading'); setError(null); setUploadProgress(0); // Step 1: Initialize console.log('1. Initializing upload...'); const initResponse = await initializeUpload(file); console.log(' Upload ID:', initResponse.upload_id); // Step 2: Calculate checksum console.log('2. Calculating checksum...'); const checksum = await calculateChecksum(file); console.log(' Checksum:', checksum); // Step 3: Upload to MinIO console.log('3. Uploading to MinIO...'); await uploadToPresignedUrl(file, initResponse.presigned_urls[0]); setUploadProgress(100); console.log(' File uploaded successfully'); // Step 4: Complete upload console.log('4. Completing upload...'); const completeResponse = await completeUpload( initResponse.upload_id, checksum ); const finalObjectId = completeResponse.id; setObjectId(finalObjectId); if (completeResponse.deduplicated) { console.log( ' File was deduplicated (already exists)', completeResponse.id ); } // Fetch metadata await fetchMetadata(finalObjectId); setUploadStatus('complete'); } catch (err) { console.error('Upload error:', err); setError(err.message); setUploadStatus('error'); } }; /** * Fetch object metadata */ const fetchMetadata = async (id) => { try { const response = await fetch(`${API_BASE}/scm/storage/${id}`, { headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error(`Fetch failed: ${response.statusText}`); } setMetadata(await response.json()); } catch (err) { console.error('Fetch metadata error:', err); } }; /** * Generate download URL */ const handleDownload = async () => { try { const response = await fetch(`${API_BASE}/scm/storage/download-url`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ object_id: objectId }), }); if (!response.ok) { throw new Error(`Download URL generation failed: ${response.statusText}`); } const { url } = await response.json(); window.open(url, '_blank'); } catch (err) { setError(`Download failed: ${err.message}`); } }; /** * Delete object (soft delete) */ const handleDelete = async () => { if (!window.confirm('Are you sure you want to delete this file?')) { return; } try { const response = await fetch(`${API_BASE}/scm/storage/${objectId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { throw new Error(`Delete failed: ${response.statusText}`); } setObjectId(null); setMetadata(null); setUploadStatus('idle'); } catch (err) { setError(`Delete failed: ${err.message}`); } }; /** * Handle file input change */ const handleFileChange = async (e) => { const file = e.target.files?.[0]; if (file) { await handleUpload(file); } }; return (

Document Upload

{/* File Input */}
{/* Upload Progress */} {uploadStatus === 'uploading' && (

Uploading: {uploadProgress}%

)} {/* Error Message */} {error && (
Error: {error}
)} {/* Success Message */} {uploadStatus === 'complete' && objectId && (
Upload Successful!

Object ID: {objectId}

)} {/* Metadata Display */} {metadata && (

File Information

File Name:
{metadata.file_name}
MIME Type:
{metadata.mime_type}
Size:
{(metadata.file_size / 1024 / 1024).toFixed(2)} MB
Domain:
{metadata.domain}
Category:
{metadata.category}
Visibility:
{metadata.visibility}
Created:
{new Date(metadata.created_at).toLocaleString()}
Checksum:
{metadata.checksum_sha256}
)} {/* Action Buttons */} {objectId && (
)}
); }; // Styling const styles = { container: { maxWidth: '600px', margin: '20px auto', padding: '20px', border: '1px solid #ddd', borderRadius: '8px', fontFamily: 'Arial, sans-serif', }, section: { marginBottom: '20px', }, label: { display: 'block', marginBottom: '8px', fontWeight: 'bold', }, input: { width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ccc', }, progressBar: { width: '100%', height: '20px', backgroundColor: '#f0f0f0', borderRadius: '4px', overflow: 'hidden', }, progressFill: { height: '100%', backgroundColor: '#4CAF50', transition: 'width 0.3s', }, error: { padding: '12px', backgroundColor: '#ffebee', color: '#c62828', borderRadius: '4px', border: '1px solid #ef5350', }, success: { padding: '12px', backgroundColor: '#e8f5e9', color: '#2e7d32', borderRadius: '4px', border: '1px solid #66bb6a', }, metadata: { display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px 16px', marginTop: '12px', }, checksum: { fontFamily: 'monospace', fontSize: '12px', wordBreak: 'break-all', }, button: { padding: '10px 20px', marginRight: '10px', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', fontWeight: 'bold', }, buttonPrimary: { backgroundColor: '#2196F3', color: 'white', }, buttonDanger: { backgroundColor: '#f44336', color: 'white', }, }; export default DocumentUploadComponent;