Skip to main content

Files Management

Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-01-0205
Version: v1.0

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
  • (fursa-{userId}) created
  • BlurHash automatically ongenerated firstfor uploadimages (use for loading placeholders)
  • All uploaded filesFiles 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 verificationpermanentUrl

File ManagementStorage FlowArchitecture

Upload
┌─────────────────────────────────────────────────────────────────────────┐
Flow

    FILE
  1. STORAGE ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ UPLOAD FLOW: │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ 1. Client sends file(s) via multipart/form-data to upload endpoint
  2. Backend validatesuploads file type,to: POST /files/upload-single │ │ │ │ 2. Backend: │ │ │ │ ├── Validates file (size, andtype) directory permissions
  3. User│ │ ├── Creates user bucket created if it doesn't existneeded (fursa-{userId})
  4. File uploaded│ │ │ ├── Generates unique filename (UUID) │ │ │ │ ├── Uploads to MinIO storage │ │ │ │ ├── If image: generates BlurHash + dimensions │ │ │ │ └── Returns file metadata with generatedpermanentUrl object key: │ └────────────────────────────────────────────────────────────────┘ │ │ │ │ STORAGE STRUCTURE: │ │ MinIO Server │ │ └── fursa-{directory}userId}/{uuid}_{filename}
  5. For images: BlurHash generated in parallel for placeholder
  6. Response returned with permanent URL, BlurHash, dimensions, and metadata

Directory Structure

Each user'User's bucket contains these predefined├── directories:

profile/ ← Profile photos │ │ │ └── abc123-uuid.jpg │ │ ├── social_post/ ← Post media │ │ │ ├── def456-uuid.jpg │ │ │ └── ghi789-uuid.mp4 │ │ ├── events/ ← Event images │ │ ├── opportunities/ ← Opportunity attachments │ │ ├── calls/ ← Call for proposals │ │ ├── funds/ ← Fund documents │ │ ├── innovation/ ← Innovation hub files │ │ └── skill_center/ ← Course materials │ │ │ │ ACCESS: │ │ └── Files accessible at: {filesServerUrl}/{bucketName}/{objectKey} │ │ └── Example: https://files.fursahub.com/fursa-uuid123/profile/pic.jpg │ │ │ └─────────────────────────────────────────────────────────────────────────┘

File Directories

Directory Purpose Typical File TypesUse
PROFILE Profile photos, avatarsphotos JPG,User PNG,avatar, WebPcover images
SOCIAL_POST Social media post images/videosposts JPG,Post PNG,images, WebP, MP4videos
CALLS Call for proposals attachments JPG,Proposal PNG, PDFdocuments
FUNDS Funding opportunity documentsFunding/grants JPG,Grant PNG,application PDFfiles
EVENTS Event banners and materialsEvents JPG,Event PNG,banners, PDFtickets
OPPORTUNITIES General Job/opportunity attachments JPG,Job PNG,post PDF, DOCattachments
INNOVATION Innovation showcase mediahub JPG,Project PNG, PDF, MP4files
SKILL_CENTER Skill/training resourcesSkills/courses JPG,Course PNG, PDF, DOCmaterials

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:

HeaderTypeRequiredDescription
AuthorizationstringYesBearer token: Bearer {accessToken}
Content-TypestringYesMust be multipart/form-data

Request Form Data Parameters:

Parameter Type Required Description Validation
filefileYesFile to uploadMax 25MB
directorystringYesTarget directoryenum: 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:

FieldDescription
fileNameGenerated unique filename
originalFileNameOriginal uploaded filename
objectKeyFull path in storage (directory/filename)
directoryStorage directory
permanentUrlPublic URL to access the file
thumbnailUrlThumbnail URL (same as permanentUrl for images)
blurHashBlurHash string for image placeholders (null for non-images)
fileTypeIMAGE, VIDEO, DOCUMENT, or OTHER
width, heightImage dimensions (null for non-images)
checksumMD5 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:

ParameterTypeRequiredDescriptionValidation
files file[] Yes Array of files to upload AtMax least25MB 1 file requiredeach
directory string Yes Target directory for uploads Must be valid FileDirectory enum value

Valid Directory Values:

ValueDescription
PROFILEProfile photos and avatars
SOCIAL_POSTSocial media post attachments
CALLSCall for proposals attachments
FUNDSFunding opportunity documents
EVENTSEvent banners and materials
OPPORTUNITIESGeneral opportunity attachments
INNOVATIONInnovation showcase media
SKILL_CENTERSkill/training resourcesvalues

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:

FieldDescription
uploadedFilesArray of successfully uploaded file details
uploadedFiles[].originalFileNameOriginal filename as uploaded by client
uploadedFiles[].storedFileNameStored filename with UUID prefix
uploadedFiles[].objectKeyFull object key path in MinIO
uploadedFiles[].permanentUrlPublic URL to access the file
uploadedFiles[].blurHashBlurHash string for image placeholders (null for non-images)
uploadedFiles[].dimensionsImage dimensions object with width/height (null for non-images)
uploadedFiles[].fileSizeFile size in bytes
uploadedFiles[].fileSizeFormattedHuman-readable file size
uploadedFiles[].contentTypeMIME type of the file
uploadedFiles[].checksumMD5 checksum for integrity verification
uploadedFiles[].directoryDirectory where file was stored
uploadedFiles[].uploadedAtUpload timestamp
totalFilesTotal number ofsome files in request
successCountNumber of successfully uploaded files
failedCountNumber of failed uploads

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"
}

Unauthorized (401):

{
  "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:

HeaderTypeRequiredDescription
AuthorizationstringYesBearer token: Bearer {accessToken}
Content-TypestringYesMust be multipart/form-data

Request Form DataPath Parameters:

encoded
Parameter Type Required Description Validation
filefileYesSingle file to uploadRequired, must be valid file
directoryobjectKey string Yes TargetFile's directoryobjectKey for(URL upload Mustif becontains valid FileDirectory enum value/)

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:

FieldDescription
originalFileNameOriginal filename as uploaded
storedFileNameStored filename with UUID prefix for uniqueness
objectKeyFull object key path in MinIO bucket
permanentUrlPublic URL to access the file directly
blurHashBlurHash placeholder string (for images only)
dimensionsWidth and height in pixels (for images only)
fileSizeFile size in bytes
fileSizeFormattedHuman-readable file size (e.g., "85.60 KB")
contentTypeMIME type of the uploaded file
checksumMD5 hash for file integrity verification
directoryDirectory where file is stored
uploadedAtISO 8601 timestamp of upload

Standard Error Types

Application-Level Exceptions (400-499)

Status CodeHTTP StatusWhen It Occurs
400BAD_REQUESTInvalid directory, file too large, unsupported file type, missing file
401UNAUTHORIZEDToken empty, invalid, or expired
403FORBIDDENAttempting to access/delete another user's files
404NOT_FOUNDFile or bucket does not exist
413PAYLOAD_TOO_LARGEFile exceeds maximum upload size
422UNPROCESSABLE_ENTITYValidation errors

Server-Level Exceptions (500+)

Status CodeHTTP StatusWhen It Occurs
500INTERNAL_SERVER_ERRORMinIO connection failure, storage errors
503SERVICE_UNAVAILABLEMinIO service is down

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 Frontend

LKO2?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 be null)

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

DirectoryAllowed TypesMax Size
PROFILEimage/jpeg, image/png, image/webp, image/gif5 MB
SOCIAL_POSTimage/jpeg, image/png, image/webp, video/mp420 MB
CALLSimage/jpeg, image/png, application/pdf10 MB
FUNDSimage/jpeg, image/png, application/pdf10 MB
EVENTSimage/jpeg, image/png, application/pdf10 MB
OPPORTUNITIESimage/jpeg, image/png, application/pdf, application/msword10 MB
INNOVATIONimage/jpeg, image/png, application/pdf, video/mp420 MB
SKILL_CENTERimage/jpeg, image/png, application/pdf, application/msword10 MB

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;
}