Files Mng
Base URL: https://api.fursahub.com/api/v1
Short Description: The File Management API handles file uploads, downloads, and management for Fursa Hub using MinIO as the object storage backend. It supports multiple file types with automatic BlurHash generation for images, organized storage by user-specific buckets and directories.
Hints:
- Each user gets their own MinIO bucket (
fursa-{userId}) created automatically on first upload - All uploaded files are publicly accessible via permanent URLs
- Images automatically generate BlurHash placeholders for smooth loading UX
- Maximum file size and allowed types are configurable per directory
- Files are organized into predefined directories (see Directory Structure below)
- File checksums (MD5) are generated for integrity verification
File Management Flow
Upload Flow
- Client sends file(s) via multipart/form-data to upload endpoint
- Backend validates file type, size, and directory permissions
- User bucket created if it doesn't exist (
fursa-{userId}) - File uploaded to MinIO with generated object key:
{directory}/{uuid}_{filename} - For images: BlurHash generated in parallel for placeholder
- Response returned with permanent URL, BlurHash, dimensions, and metadata
Directory Structure
Each user's bucket contains these predefined directories:
| Directory | Purpose | Typical File Types |
|---|---|---|
PROFILE |
Profile photos, avatars | JPG, PNG, WebP |
SOCIAL_POST |
Social media post images/videos | JPG, PNG, WebP, MP4 |
CALLS |
Call for proposals attachments | JPG, PNG, PDF |
FUNDS |
Funding opportunity documents | JPG, PNG, PDF |
EVENTS |
Event banners and materials | JPG, PNG, PDF |
OPPORTUNITIES |
General opportunity attachments | JPG, PNG, PDF, DOC |
INNOVATION |
Innovation showcase media | JPG, PNG, PDF, MP4 |
SKILL_CENTER |
Skill/training resources | JPG, PNG, PDF, DOC |
URL Structure
Permanent file URLs follow this pattern:
{minio-endpoint}/{bucket-name}/{directory}/{uuid}_{filename}
Example:
http://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/profile/a1b2c3d4_photo.jpg
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-01-02T10:30:45",
"data": "Error description"
}
Endpoints
1. Upload Multiple Files
Purpose: Upload one or more files to a specified directory in the user's storage bucket.
Endpoint: POST {base_url}/files/upload
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be multipart/form-data |
Request Form Data Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
files |
file[] | Yes | Array of files to upload | At least 1 file required |
directory |
string | Yes | Target directory for uploads | Must be valid FileDirectory enum value |
Valid Directory Values:
| Value | Description |
|---|---|
PROFILE |
Profile photos and avatars |
SOCIAL_POST |
Social media post attachments |
CALLS |
Call for proposals attachments |
FUNDS |
Funding opportunity documents |
EVENTS |
Event banners and materials |
OPPORTUNITIES |
General opportunity attachments |
INNOVATION |
Innovation showcase media |
SKILL_CENTER |
Skill/training resources |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Files uploaded successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"uploadedFiles": [
{
"originalFileName": "profile-photo.jpg",
"storedFileName": "a1b2c3d4-e5f6-7890-abcd-ef1234567890_profile-photo.jpg",
"objectKey": "profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_profile-photo.jpg",
"permanentUrl": "http://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_profile-photo.jpg",
"blurHash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
"dimensions": {
"width": 800,
"height": 600
},
"fileSize": 245678,
"fileSizeFormatted": "239.92 KB",
"contentType": "image/jpeg",
"checksum": "d41d8cd98f00b204e9800998ecf8427e",
"directory": "PROFILE",
"uploadedAt": "2025-01-02T10:30:45"
},
{
"originalFileName": "event-banner.pdf",
"storedFileName": "b2c3d4e5-f6a7-8901-bcde-f12345678901_event-banner.pdf",
"objectKey": "events/b2c3d4e5-f6a7-8901-bcde-f12345678901_event-banner.pdf",
"permanentUrl": "http://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/events/b2c3d4e5-f6a7-8901-bcde-f12345678901_event-banner.pdf",
"blurHash": null,
"dimensions": null,
"fileSize": 1048576,
"fileSizeFormatted": "1.00 MB",
"contentType": "application/pdf",
"checksum": "e99a18c428cb38d5f260853678922e03",
"directory": "EVENTS",
"uploadedAt": "2025-01-02T10:30:45"
}
],
"totalFiles": 2,
"successCount": 2,
"failedCount": 0
}
}
Success Response Fields:
| Field | Description |
|---|---|
uploadedFiles |
Array of successfully uploaded file details |
uploadedFiles[].originalFileName |
Original filename as uploaded by client |
uploadedFiles[].storedFileName |
Stored filename with UUID prefix |
uploadedFiles[].objectKey |
Full object key path in MinIO |
uploadedFiles[].permanentUrl |
Public URL to access the file |
uploadedFiles[].blurHash |
BlurHash string for image placeholders (null for non-images) |
uploadedFiles[].dimensions |
Image dimensions object with width/height (null for non-images) |
uploadedFiles[].fileSize |
File size in bytes |
uploadedFiles[].fileSizeFormatted |
Human-readable file size |
uploadedFiles[].contentType |
MIME type of the file |
uploadedFiles[].checksum |
MD5 checksum for integrity verification |
uploadedFiles[].directory |
Directory where file was stored |
uploadedFiles[].uploadedAt |
Upload timestamp |
totalFiles |
Total number of files in request |
successCount |
Number of successfully uploaded files |
failedCount |
Number of failed uploads |
Error Response JSON Samples:
Invalid Directory (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid directory: INVALID_DIR",
"action_time": "2025-01-02T10:30:45",
"data": "Invalid directory: INVALID_DIR. Valid directories: PROFILE, SOCIAL_POST, CALLS, FUNDS, EVENTS, OPPORTUNITIES, INNOVATION, SKILL_CENTER"
}
File Too Large (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "File size exceeds maximum allowed",
"action_time": "2025-01-02T10:30:45",
"data": "File 'large-video.mp4' exceeds maximum size of 10MB"
}
Unsupported File Type (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Unsupported file type",
"action_time": "2025-01-02T10:30:45",
"data": "File type 'application/x-executable' is not allowed"
}
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Token is missing or invalid",
"action_time": "2025-01-02T10:30:45",
"data": "Token is missing or invalid"
}
2. Upload Single File
Purpose: Upload a single file with simplified response. Ideal for profile photo uploads or single document submissions.
Endpoint: POST {base_url}/files/upload-single
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token: Bearer {accessToken} |
| Content-Type | string | Yes | Must be multipart/form-data |
Request Form Data Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
file |
file | Yes | Single file to upload | Required, must be valid file |
directory |
string | Yes | Target directory for upload | Must be valid FileDirectory enum value |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "File uploaded successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"originalFileName": "my-photo.png",
"storedFileName": "c3d4e5f6-a7b8-9012-cdef-123456789012_my-photo.png",
"objectKey": "profile/c3d4e5f6-a7b8-9012-cdef-123456789012_my-photo.png",
"permanentUrl": "http://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/profile/c3d4e5f6-a7b8-9012-cdef-123456789012_my-photo.png",
"blurHash": "LGF5]+Yk^6#M@-5c,1J5@[or[Q6.",
"dimensions": {
"width": 400,
"height": 400
},
"fileSize": 87654,
"fileSizeFormatted": "85.60 KB",
"contentType": "image/png",
"checksum": "098f6bcd4621d373cade4e832627b4f6",
"directory": "PROFILE",
"uploadedAt": "2025-01-02T10:30:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
originalFileName |
Original filename as uploaded |
storedFileName |
Stored filename with UUID prefix for uniqueness |
objectKey |
Full object key path in MinIO bucket |
permanentUrl |
Public URL to access the file directly |
blurHash |
BlurHash placeholder string (for images only) |
dimensions |
Width and height in pixels (for images only) |
fileSize |
File size in bytes |
fileSizeFormatted |
Human-readable file size (e.g., "85.60 KB") |
contentType |
MIME type of the uploaded file |
checksum |
MD5 hash for file integrity verification |
directory |
Directory where file is stored |
uploadedAt |
ISO 8601 timestamp of upload |
3. List Directory Contents
Purpose: List all files in a specific directory within the user's storage bucket.
Endpoint: GET {base_url}/files/directory/{directory}
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
|
Path Parameters:
|
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Directory contents retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"directory": "SOCIAL_POST",
"files": [
{
"objectKey": "social_post/a1b2c3d4-e5f6-7890-abcd-ef1234567890_photo1.jpg",
"fileName": "a1b2c3d4-e5f6-7890-abcd-ef1234567890_photo1.jpg",
"permanentUrl": "http://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/social_post/a1b2c3d4-e5f6-7890-abcd-ef1234567890_photo1.jpg",
"fileSize": 245678,
"fileSizeFormatted": "239.92 KB",
"contentType": "image/jpeg",
"lastModified": "2025-01-02T10:30:45"
},
{
"objectKey": "social_post/b2c3d4e5-f6a7-8901-bcde-f12345678901_video.mp4",
"fileName": "b2c3d4e5-f6a7-8901-bcde-f12345678901_video.mp4",
"permanentUrl": "http://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/social_post/b2c3d4e5-f6a7-8901-bcde-f12345678901_video.mp4",
"fileSize": 5242880,
"fileSizeFormatted": "5.00 MB",
"contentType": "video/mp4",
"lastModified": "2025-01-01T15:20:30"
}
],
"totalFiles": 2,
"totalSize": 5488558,
"totalSizeFormatted": "5.23 MB"
}
}
Success Response Fields:
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
Error Response JSON Samples:
Invalid Directory (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid directory",
"action_time": "2025-01-02T10:30:45",
"data": "Invalid directory: PHOTOS. Valid directories: PROFILE, SOCIAL_POST, CALLS, FUNDS, EVENTS, OPPORTUNITIES, INNOVATION, SKILL_CENTER"
}
Empty Directory (200 - Not an error):
{
"success": true,
"httpStatus": "OK",
"message": "Directory contents retrieved",
"action_time": "2025-01-02T10:30:45",
"data": {
"directory": "INNOVATION",
"files": [],
"totalFiles": 0,
"totalSize": 0,
"totalSizeFormatted": "0 B"
}
}
4. Delete File
Purpose: Delete a specific file from the user's storage bucket.
Endpoint: DELETE {base_url}/files/{objectKey}
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
|
Path Parameters:
| |
Important Note: The objectKey must be URL-encoded since it contains forward slashes. For example:
Original:profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_photo.jpgEncoded:profile%2Fa1b2c3d4-e5f6-7890-abcd-ef1234567890_photo.jpg
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "File deleted successfully",
"action_time": "2025-01-02T10:30:45",
"data": {
"deletedObjectKey": "profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_photo.jpg",
"deletedAt": "2025-01-02T10:30:45"
}
}
Success Response Fields:
| |
|
Error Response JSON Samples:
File Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "File not found",
"action_time": "2025-01-02T10:30:45",
"data": "File not found: profile/nonexistent-file.jpg"
}
{
"success": false,
"httpStatus": "FORBIDDEN",
"message": "Access denied",
"action_time": "2025-01-02T10:30:45",
"data": "You can only delete files from your own bucket"
}
Standard Error Types
Application-Level Exceptions (400-499)
| Status Code | HTTP Status | When It Occurs |
|---|---|---|
| 400 | BAD_REQUEST | Invalid directory, file too large, unsupported file type, missing file |
| 401 | UNAUTHORIZED | Token empty, invalid, or expired |
| 403 | FORBIDDEN | Attempting to access/delete another user's files |
| 404 | NOT_FOUND | File or bucket does not exist |
| 413 | PAYLOAD_TOO_LARGE | File exceeds maximum upload size |
| 422 | UNPROCESSABLE_ENTITY | Validation errors |
Server-Level Exceptions (500+)
| Status Code | HTTP Status | When It Occurs |
|---|---|---|
| 500 | INTERNAL_SERVER_ERROR | MinIO connection failure, storage errors |
| 503 | SERVICE_UNAVAILABLE | MinIO service is down |
BlurHash Explained
BlurHash is a compact representation of a placeholder for an image. It allows you to show a blurred preview while the actual image loads.
How to Use BlurHash on Frontend
React Native Example:
import { Blurhash } from 'react-native-blurhash';
<Blurhash
blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
width={200}
height={150}
/>
Web (Canvas) Example:
import { decode } from 'blurhash';
const pixels = decode("LEHV6nWB2yk8pyo0adR*.7kCMdnj", 32, 32);
// Render pixels to canvas
When BlurHash is Generated
- ✅ Image files: JPG, JPEG, PNG, WebP, GIF
- ❌ Non-image files: PDF, DOC, MP4, etc. (blurHash will be
null)
Frontend Implementation Guide
Upload Flow Example (React Native)
const uploadFile = async (file, directory) => {
const formData = new FormData();
formData.append('file', {
uri: file.uri,
type: file.type,
name: file.fileName,
});
formData.append('directory', directory);
const response = await fetch(`${BASE_URL}/files/upload-single`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'multipart/form-data',
},
body: formData,
});
return response.json();
};
Image Loading with BlurHash Placeholder
const ImageWithPlaceholder = ({ file }) => {
const [loaded, setLoaded] = useState(false);
return (
<View>
{!loaded && file.blurHash && (
<Blurhash
blurhash={file.blurHash}
width={file.dimensions.width}
height={file.dimensions.height}
/>
)}
<Image
source={{ uri: file.permanentUrl }}
onLoad={() => setLoaded(true)}
style={{ display: loaded ? 'flex' : 'none' }}
/>
</View>
);
};
Quick Reference
Supported File Types by Directory
| Directory | Allowed Types | Max Size |
|---|---|---|
PROFILE |
image/jpeg, image/png, image/webp, image/gif | 5 MB |
SOCIAL_POST |
image/jpeg, image/png, image/webp, video/mp4 | 20 MB |
CALLS |
image/jpeg, image/png, application/pdf | 10 MB |
FUNDS |
image/jpeg, image/png, application/pdf | 10 MB |
EVENTS |
image/jpeg, image/png, application/pdf | 10 MB |
OPPORTUNITIES |
image/jpeg, image/png, application/pdf, application/msword | 10 MB |
INNOVATION |
image/jpeg, image/png, application/pdf, video/mp4 | 20 MB |
SKILL_CENTER |
image/jpeg, image/png, application/pdf, application/msword | 10 MB |
FileDirectory Enum Values
public enum FileDirectory {
PROFILE,
SOCIAL_POST,
CALLS,
FUNDS,
EVENTS,
OPPORTUNITIES,
INNOVATION,
SKILL_CENTER
}
MinIO Configuration
minio:
endpoint: ${MINIO_ENDPOINT:http://localhost:9000}
access-key: ${MINIO_ACCESS_KEY:minioadmin}
secret-key: ${MINIO_SECRET_KEY:minioadmin}
bucket-prefix: fursa-