cuatrolabs-scm-ms / DocumentUploadComponent.jsx
MukeshKapoor25's picture
minio doc store
9b11567
/**
* 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 (
<div style={styles.container}>
<h2>Document Upload</h2>
{/* File Input */}
<div style={styles.section}>
<label htmlFor="file-input" style={styles.label}>
Select File:
</label>
<input
id="file-input"
ref={fileInputRef}
type="file"
onChange={handleFileChange}
disabled={uploadStatus === 'uploading'}
style={styles.input}
/>
</div>
{/* Upload Progress */}
{uploadStatus === 'uploading' && (
<div style={styles.section}>
<p>Uploading: {uploadProgress}%</p>
<div style={styles.progressBar}>
<div
style={{
...styles.progressFill,
width: `${uploadProgress}%`,
}}
/>
</div>
</div>
)}
{/* Error Message */}
{error && (
<div style={{ ...styles.section, ...styles.error }}>
<strong>Error:</strong> {error}
</div>
)}
{/* Success Message */}
{uploadStatus === 'complete' && objectId && (
<div style={{ ...styles.section, ...styles.success }}>
<strong>Upload Successful!</strong>
<p>Object ID: {objectId}</p>
</div>
)}
{/* Metadata Display */}
{metadata && (
<div style={styles.section}>
<h3>File Information</h3>
<dl style={styles.metadata}>
<dt>File Name:</dt>
<dd>{metadata.file_name}</dd>
<dt>MIME Type:</dt>
<dd>{metadata.mime_type}</dd>
<dt>Size:</dt>
<dd>{(metadata.file_size / 1024 / 1024).toFixed(2)} MB</dd>
<dt>Domain:</dt>
<dd>{metadata.domain}</dd>
<dt>Category:</dt>
<dd>{metadata.category}</dd>
<dt>Visibility:</dt>
<dd>{metadata.visibility}</dd>
<dt>Created:</dt>
<dd>{new Date(metadata.created_at).toLocaleString()}</dd>
<dt>Checksum:</dt>
<dd style={styles.checksum}>{metadata.checksum_sha256}</dd>
</dl>
</div>
)}
{/* Action Buttons */}
{objectId && (
<div style={styles.section}>
<button
onClick={handleDownload}
style={{ ...styles.button, ...styles.buttonPrimary }}
>
Download
</button>
<button
onClick={handleDelete}
style={{ ...styles.button, ...styles.buttonDanger }}
>
Delete
</button>
</div>
)}
</div>
);
};
// 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;