Skip to main content

Files Mng

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

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

  1. Client sends file(s) via multipart/form-data to upload endpoint
  2. Backend validates file type, size, and directory permissions
  3. User bucket created if it doesn't exist (fursa-{userId})
  4. File uploaded to MinIO with generated object key: {directory}/{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'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"
}

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. 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:

Header Type Required Description
Authorization string Yes Bearer token: Bearer {accessToken}

Path Parameters:

Parameter Type Required Description Validation
directory string Yes Directory to list contents from Must be valid FileDirectory enum value (case-insensitive)

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:

Field Description
directory The directory that was listed
files Array of file objects in the directory
files[].objectKey Full object key path
files[].fileName Just the filename portion
files[].permanentUrl Public URL to access the file
files[].fileSize File size in bytes
files[].fileSizeFormatted Human-readable file size
files[].contentType MIME type of the file
files[].lastModified Last modification timestamp
totalFiles Total count of files in directory
totalSize Combined size of all files in bytes
totalSizeFormatted Human-readable total size

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:

Header Type Required Description
Authorization string Yes Bearer token: Bearer {accessToken}

Path Parameters:

Parameter Type Required Description Validation
objectKey string Yes Full object key of the file to delete URL-encoded object key (e.g., profile%2Fa1b2c3d4_photo.jpg)

Important Note: The objectKey must be URL-encoded since it contains forward slashes. For example:

  • Original: profile/a1b2c3d4-e5f6-7890-abcd-ef1234567890_photo.jpg
  • Encoded: 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:

Field Description
deletedObjectKey The object key of the deleted file
deletedAt Timestamp when the file was deleted

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

Unauthorized - Not Owner (403):

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