Skip to main content

Files Management

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

Base URL: https://api.fursahub.com/api/v1

Short Description: File upload and management endpoints for Fursa Hub. Handles image uploads for profiles, posts, events, and other content. Uses MinIO for storage with automatic BlurHash generation for 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 storage bucket
  • BlurHash automatically generated for images (use for loading placeholders)
  • Files are publicly accessible via permanentUrl

File Storage Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                        FILE STORAGE ARCHITECTURE                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  UPLOAD FLOW:                                                           │
│  ┌────────────────────────────────────────────────────────────────┐     │
│  │ 1. Client uploads file to: POST /files/upload-single           │     │
│  │ 2. Backend:                                                     │     │
│  │    ├── Validates file (size, type)                             │     │
│  │    ├── Creates user bucket if needed (fursa-{userId})          │     │
│  │    ├── Generates unique filename (UUID)                        │     │
│  │    ├── Uploads to MinIO storage                                │     │
│  │    ├── If image: generates BlurHash + dimensions               │     │
│  │    └── Returns file metadata with permanentUrl                 │     │
│  └────────────────────────────────────────────────────────────────┘     │
│                                                                          │
│  STORAGE STRUCTURE:                                                      │
│  MinIO Server                                                            │
│  └── fursa-{userId}/                    ← User's bucket                 │
│      ├── 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 Use
PROFILE Profile photos User avatar, cover images
SOCIAL_POST Social media posts Post images, videos
CALLS Call for proposals Proposal documents
FUNDS Funding/grants Grant application files
EVENTS Events Event banners, tickets
OPPORTUNITIES Job/opportunity Job post attachments
INNOVATION Innovation hub Project files
SKILL_CENTER Skills/courses Course materials

Endpoints


1. Upload Single File

Purpose: Upload a single file to the specified directory.

Endpoint: POST {base_url}/files/upload-single

Access Level: 🔒 Protected

Authentication: Bearer Token

Content-Type: multipart/form-data

Request 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 Max 25MB each
directory string Yes Target directory enum values

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Files uploaded successfully",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "uploadedFiles": [
      {
        "fileName": "uuid1.jpg",
        "originalFileName": "photo1.jpg",
        "permanentUrl": "https://files.fursahub.com/bucket/path/uuid1.jpg",
        "blurHash": "LKO2?U%2Tw=w]...",
        "isImage": true
      },
      {
        "fileName": "uuid2.jpg",
        "originalFileName": "photo2.jpg",
        "permanentUrl": "https://files.fursahub.com/bucket/path/uuid2.jpg",
        "blurHash": "LAB2?Q%1Tu=x]...",
        "isImage": true
      }
    ],
    "totalFiles": 2,
    "successfulUploads": 2,
    "failedUploads": 0,
    "totalSize": 512000,
    "totalSizeFormatted": "500.0 KB",
    "uploadedAt": "2025-01-05T10:35:00",
    "message": "2 files uploaded successfully",
    "errors": null
  }
}

Partial Success Response (some files failed):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Files uploaded successfully",
  "action_time": "2025-01-05T10:35:00",
  "data": {
    "uploadedFiles": [...],
    "totalFiles": 3,
    "successfulUploads": 2,
    "failedUploads": 1,
    "message": "2 files uploaded successfully, 1 failed",
    "errors": [
      "Failed to upload large-file.zip: File size exceeds maximum limit of 25MB"
    ]
  }
}

3. Delete File

Purpose: Delete a file from storage.

Endpoint: DELETE {base_url}/files/{objectKey}

Access Level: 🔒 Protected

Authentication: Bearer Token

Path Parameters:

Parameter Type Required Description
objectKey string Yes File's objectKey (URL encoded if contains /)

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "File deleted successfully",
  "action_time": "2025-01-05T10:40:00",
  "data": null
}

BlurHash Usage

BlurHash is a compact representation of an image placeholder. Use it to show a blurred preview while the actual image loads.

Example BlurHash: LKO2?U%2Tw=w]~RBVZRi};RPxuwH

Frontend Implementation:

// 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: 'relative' }}>
      {!loaded && blurHash && (
        <Blurhash
          hash={blurHash}
          width={width}
          height={height}
          resolutionX={32}
          resolutionY={32}
        />
      )}
      <img 
        src={imageUrl}
        onLoad={() => setLoaded(true)}
        style={{ display: loaded ? 'block' : 'none' }}
      />
    </div>
  );
}

Frontend Implementation Guide

Profile Photo Upload

1. User selects photo
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', file);
  formData.append('directory', directory);
  
  return fetch('{base_url}/files/upload-single', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`
    },
    body: formData
  });
}

Validation Before Upload

const MAX_SIZE = 25 * 1024 * 1024; // 25MB
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];

function validateFile(file) {
  if (file.size > MAX_SIZE) {
    throw new Error('File too large (max 25MB)');
  }
  if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
    throw new Error('Invalid file type');
  }
  return true;
}