Files Management
Base URL: https://api.fursahub.com/api/v1
Short Description: The File Management API handles file uploads, downloads,upload and management endpoints for Fursa HubHub. usingHandles image uploads for profiles, posts, events, and other content. Uses MinIO as the objectfor storage backend. It supports multiple file types with automatic BlurHash generation for images, organized storage by user-specific buckets and directories.images.
Hints:
- Maximum file size: 25MB per file
- Supported image types: JPEG, PNG, GIF, WebP, BMP
- Supported video types: MP4, AVI, MOV, WebM, MKV
- Supported documents: PDF, Word, Excel, Text files
- Each user gets their own
MinIOstorage bucket - BlurHash automatically
ongeneratedfirstforuploadimages (use for loading placeholders) All uploaded filesFiles are publicly accessible viapermanent URLsImages automatically generate BlurHash placeholders for smooth loading UXMaximum file size and allowed types are configurable per directoryFiles are organized into predefined directories (see Directory Structure below)File checksums (MD5) are generated for integrity verificationpermanentUrl
fursa-{userId}File ManagementStorage FlowArchitecture
Upload┌─────────────────────────────────────────────────────────────────────────┐
Flow
│ ┌─────────────────────────────────────────────────────────────────────────┐
Flow- FILE
- STORAGE ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ UPLOAD FLOW: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ 1. Client
sends file(s)via multipart/form-data to upload endpoint Backend validatesuploads filetype,to: POST /files/upload-single │ │ │ │ 2. Backend: │ │ │ │ ├── Validates file (size,andtype)directory│permissions│
User│ │ ├── Creates user bucketcreatedifit doesn't existneeded (fursa-{userId})File│uploaded│ │ │ ├── Generates unique filename (UUID) │ │ │ │ ├── Uploads to MinIO storage │ │ │ │ ├── If image: generates BlurHash + dimensions │ │ │ │ └── Returns file metadata withgeneratedpermanentUrlobject│key:││ └────────────────────────────────────────────────────────────────┘ │ │ │ │ STORAGE STRUCTURE: │ │ MinIO Server │ │ └── fursa-{directory}userId}/{uuid}_{filename}For←images: BlurHash generated in parallel for placeholderResponse returnedwith permanent URL, BlurHash, dimensions, and metadata
Directory Structure
Each user'User's bucket contains│
these│ predefined├── directories:
File Directories
| Directory | Purpose | Typical |
|---|---|---|
PROFILE |
Profile |
|
SOCIAL_POST |
Social media |
|
CALLS |
Call for proposals |
|
FUNDS |
||
EVENTS |
||
OPPORTUNITIES |
||
INNOVATION |
Innovation |
|
SKILL_CENTER |
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 MultipleSingle FilesFile
Purpose: Upload onea orsingle more filesfile to athe specified directory in the user's storage bucket.directory.
Endpoint: POST {base_url}/files/uploadupload-single
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request HeadersContent-Type:
| |||
multipart/form-data |
Request Form Data Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| file | file | Yes | File to upload | Max 25MB |
| directory | string | Yes | Target directory | enum: PROFILE, SOCIAL_POST, CALLS, FUNDS, EVENTS, OPPORTUNITIES, INNOVATION, SKILL_CENTER |
Example Request (using curl):
curl -X POST \
-H "Authorization: Bearer {accessToken}" \
-F "file=@/path/to/image.jpg" \
-F "directory=PROFILE" \
{base_url}/files/upload-single
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "File uploaded successfully",
"action_time": "2025-01-05T10:30:45",
"data": {
"fileName": "550e8400-e29b-41d4-a716-446655440000.jpg",
"originalFileName": "my-photo.jpg",
"objectKey": "profile/550e8400-e29b-41d4-a716-446655440000.jpg",
"directory": "PROFILE",
"contentType": "image/jpeg",
"fileSize": 245678,
"fileSizeFormatted": "239.9 KB",
"permanentUrl": "https://files.fursahub.com/fursa-userid/profile/550e8400.jpg",
"thumbnailUrl": "https://files.fursahub.com/fursa-userid/profile/550e8400.jpg",
"blurHash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
"fileExtension": ".jpg",
"fileType": "IMAGE",
"isImage": true,
"isVideo": false,
"isDocument": false,
"width": 1920,
"height": 1080,
"dimensions": "1920x1080",
"checksum": "d41d8cd98f00b204e9800998ecf8427e",
"uploadedAt": "2025-01-05T10:30:45",
"uploadedBy": "550e8400-e29b-41d4-a716-446655440000",
"isPublic": true
}
}
Success Response Fields:
| Field | Description |
|---|---|
| fileName | Generated unique filename |
| originalFileName | Original uploaded filename |
| objectKey | Full path in storage (directory/filename) |
| directory | Storage directory |
| permanentUrl | Public URL to access the file |
| thumbnailUrl | Thumbnail URL (same as permanentUrl for images) |
| blurHash | BlurHash string for image placeholders (null for non-images) |
| fileType | IMAGE, VIDEO, DOCUMENT, or OTHER |
| width, height | Image dimensions (null for non-images) |
| checksum | MD5 checksum for integrity verification |
Error Responses:
File Too Large (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "File size exceeds maximum limit of 25MB",
"action_time": "2025-01-05T10:30:45",
"data": "File size exceeds maximum limit of 25MB"
}
Empty File (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "File is empty",
"action_time": "2025-01-05T10:30:45",
"data": "File is empty"
}
2. Upload Multiple Files
Purpose: Upload multiple files at once to the same directory.
Endpoint: POST {base_url}/files/upload
Access Level: 🔒 Protected
Authentication: Bearer Token
Content-Type: multipart/form-data
Request Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| files | file[] | Yes | Array of files |
|
directory |
string | Yes | Target directory |
Valid Directory Values:
| |
| |
| |
| |
| |
| |
| |
|
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Files uploaded successfully",
"action_time": "2025-01-02T10:30:45"05T10:35:00",
"data": {
"uploadedFiles": [
{
"fileName": "uuid1.jpg",
"originalFileName": "profile-photo.jpg",
"storedFileName": "a1b2c3d4-e5f6-7890-abcd-ef1234567890_profile-photo.jpg",
"objectKey": "profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_profile-photo.photo1.jpg",
"permanentUrl": "http:https://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_profile-photo.files.fursahub.com/bucket/path/uuid1.jpg",
"blurHash": "LEHV6nWB2yk8pyo0adR*LKO2?U%2Tw=w].7kCMdnj"..",
"dimensions"isImage": {
"width": 800,
"height": 600
},
"fileSize": 245678,
"fileSizeFormatted": "239.92 KB",
"contentType": "image/jpeg",
"checksum": "d41d8cd98f00b204e9800998ecf8427e",
"directory": "PROFILE",
"uploadedAt": "2025-01-02T10:30:45"true
},
{
"fileName": "uuid2.jpg",
"originalFileName": "event-banner.pdf",
"storedFileName": "b2c3d4e5-f6a7-8901-bcde-f12345678901_event-banner.pdf",
"objectKey": "events/b2c3d4e5-f6a7-8901-bcde-f12345678901_event-banner.pdf"photo2.jpg",
"permanentUrl": "http:https://localhost:9000/fursa-550e8400-e29b-41d4-a716-446655440000/events/b2c3d4e5-f6a7-8901-bcde-f12345678901_event-banner.pdf"files.fursahub.com/bucket/path/uuid2.jpg",
"blurHash": null,
"dimensions": null,
LAB2?Q%1Tu=x]..."fileSize": 1048576,
"fileSizeFormatted": "1.00 MB",
"contentType"isImage": "application/pdf",
"checksum": "e99a18c428cb38d5f260853678922e03",
"directory": "EVENTS",
"uploadedAt": "2025-01-02T10:30:45"true
}
],
"totalFiles": 2,
"successCount"successfulUploads": 2,
"failedCount"failedUploads": 0,
"totalSize": 512000,
"totalSizeFormatted": "500.0 KB",
"uploadedAt": "2025-01-05T10:35:00",
"message": "2 files uploaded successfully",
"errors": null
}
}
Partial Success Response Fields:
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
Error Response JSON Samplesfailed):
Invalid Directory (400):
{
"success": false,true,
"httpStatus": "BAD_REQUEST"OK",
"message": "InvalidFiles directory:uploaded INVALID_DIR"successfully",
"action_time": "2025-01-02T10:30:45"05T10:35:00",
"data": "Invalid directory: INVALID_DIR. Valid directories: PROFILE, SOCIAL_POST, CALLS, FUNDS, EVENTS, OPPORTUNITIES, INNOVATION, SKILL_CENTER"
}
File Too Large (400):
{
"success"uploadedFiles": false,[...],
"httpStatus"totalFiles": 3,
"BAD_REQUEST",successfulUploads": 2,
"failedUploads": 1,
"message": "2 files uploaded successfully, 1 failed",
"errors": [
"Failed to upload large-file.zip: File size exceeds maximum allowed",
"action_time": "2025-01-02T10:30:45",
"data": "File 'large-video.mp4' exceeds maximum sizelimit of 10MB"25MB"
]
}
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.3. Upload SingleDelete File
Purpose: UploadDelete a single file withfrom simplified response. Ideal for profile photo uploads or single document submissions.storage.
Endpoint: POSTDELETE {base_url}/files/upload-single{objectKey}
Access Level: 🔒 Protected (Requires valid access token)
Authentication: Bearer Token
Request Headers:
| |||
|
Request Form DataPath Parameters:
| Parameter | Type | Required | Description | |
|---|---|---|---|---|
| ||||
objectKey |
string | Yes |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "File uploadeddeleted successfully",
"action_time": "2025-01-02T10:30:45"05T10:40:00",
"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"
}null
}
Success Response Fields:
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
Standard Error Types
Application-Level Exceptions (400-499)
Server-Level Exceptions (500+)
BlurHash ExplainedUsage
BlurHash is a compact representation of a placeholder for an image.image Itplaceholder. allowsUse youit to show a blurred preview while the actual image loads.
How to Use
Example BlurHash: on FrontendLKO2?U%2Tw=w]~RBVZRi};RPxuwH
ReactFrontend Native ExampleImplementation:
// React example with blurhash library
import { Blurhash } from "react-blurhash";
function ImageWithPlaceholder({ imageUrl, blurHash, width, height }) {
const [loaded, setLoaded] = useState(false);
return (
<div style={{ position: 'react-native-blurhash';relative' }}>
{!loaded && blurHash && (
<Blurhash
blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"hash={blurHash}
width={200}width}
height={150}height}
resolutionX={32}
resolutionY={32}
/>
)}
Web<img
src={imageUrl}
onLoad={(Canvas)) Example:
importsetLoaded(true)}
style={{ decodedisplay: loaded ? 'block' : 'none' }}
/>
</div>
);
}
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 benull)
Frontend Implementation Guide
Profile Photo Upload Flow Example (React Native)
const1. uploadFileUser =selects asyncphoto
(2. POST /files/upload-single
├── file: selected image
└── directory: "PROFILE"
3. Get permanentUrl from response
4. POST /profile/photo with photoUrl
5. Display image with blurHash placeholder
Social Post with Images
1. User creates post, selects images
2. For each image: POST /files/upload-single
├── file: image
└── directory: "SOCIAL_POST"
3. Collect all permanentUrls
4. Create post with image URLs array
5. Store blurHash for each image for feed display
File Upload Component
function uploadFile(file, directory) => {
const formData = new FormData();
formData.append('file', {
uri: file.uri,
type: file.type,
name: file.fileName,
})file);
formData.append('directory', directory);
const response = awaitreturn fetch(`$'{BASE_URL}base_url}/files/upload-single`single', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'multipart/form-data',
},
body: formData,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
| ||
| ||
| ||
| ||
| ||
| ||
| ||
|
FileDirectory Enum Values
public enum FileDirectory {
PROFILE,
SOCIAL_POST,
CALLS,
FUNDS,
EVENTS,
OPPORTUNITIES,
INNOVATION,
SKILL_CENTER
}
MinIOValidation ConfigurationBefore Upload
minio:const endpoint:MAX_SIZE ${MINIO_ENDPOINT:http:= 25 * 1024 * 1024; //localhost:9000} access-key:25MB
$const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
function validateFile(file) {MINIO_ACCESS_KEY:minioadmin}
secret-key:if $(file.size > MAX_SIZE) {MINIO_SECRET_KEY:minioadmin}
bucket-prefix:throw fursa-new Error('File too large (max 25MB)');
}
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
throw new Error('Invalid file type');
}
return true;
}