API Reference

v1.1
Sign In

Introduction

The GrimoireOS API is a powerful RESTful interface that enables you to programmatically manage your files and storage.

Base URL

https://manastorage.com/api/v1

Format

JSON

Authentication

HMAC-SHA256
Ready to get started?
Check out our Quickstart guide to make your first API call in minutes.
Important Notice
Api Version 1.1 will be deprecated before the end of the year. We will be releasing an SDK across multiple languages in order to decrease the complexity of our api.

Quickstart

Get up and running with the GrimoireOS API in just 3 steps:

1

Create an API Key

Sign in and navigate to API Keys to generate your credentials.

2

Set up Authentication

Use HMAC-SHA256 to sign your requests with your API secret.

3

Make Your First Request

Start with a simple GET request to list your files.

Libraries

While we don't have official SDKs yet, you can use standard HTTP libraries in any language. Here are some recommendations:

Python

requests

Node.js

axios or fetch

PHP

cURL or Guzzle

Ruby

HTTParty

Authentication Overview

The GrimoireOS API uses HMAC-SHA256 signature verification to ensure secure communication.

Authentication Flow

  1. Create a canonical string from your request details
  2. Sign it with your API secret using HMAC-SHA256
  3. Include the signature in your request headers
  4. The server validates your signature

File Upload Limits & Chunking

The API supports files of any size through automatic chunked uploads. Here's what you need to know:

Small Files (< 10MB)

Uploaded directly without chunking

Large Files (≥ 10MB)

Automatically split into chunks and uploaded

Chunk Size

Default: 5MB, Maximum: 90MB

Rate Limiting

Built-in delays prevent server overload

Note: The server enforces a maximum chunk size of 90MB. Attempting to use larger chunks will result in a 413 error.

API Keys

API keys consist of two parts:

API Key ID

A public identifier that starts with MS_live_

Example: MS_live_abc123def456

API Secret

A private key used for signing requests

⚠️ Only shown once when created!

Security Notice
Store your API secret securely. It cannot be retrieved after creation.

HMAC Signature

Required Headers

Header Description
X-Api-Key Your API Key ID
X-Api-Timestamp Unix timestamp (seconds)
X-Api-Nonce Unique request ID (min 16 chars)
X-Api-Signature Base64 encoded HMAC signature

Creating the Signature

1. Build canonical string:
METHOD
PATH_WITH_QUERY
BODY_HASH
API_KEY_ID
TIMESTAMP
NONCE

2. Sign with HMAC-SHA256:
signature = HMAC-SHA256(api_secret, canonical_string)

3. Base64 encode:
encoded_signature = base64(signature)

Files

Manage your files through the API

Upload file

POST /api/v1/files

Upload a new file to your storage.

Request Body (multipart/form-data)

Parameter Type Required Description
file file Yes The file to upload
password string No Password protect the file
expiry_days integer No Number of days until file expires (1-365)
notes string No Notes about the file (max 25 chars)

Response

{
  "success": true,
  "message": "File uploaded successfully",
  "data": {
    "id": "abc123def456",
    "name": "backup.zip",
    "size": 1048576,
    "type": "file",
    "checksum": "sha256_hash",
    "download_url": "https://manastorage.com/download/abc123def456",
    "created_at": "2024-01-15T10:30:00Z",
    "expires_at": "2024-02-15T10:30:00Z"
  }
}

Chunked Upload Parameters

For large files that are automatically chunked, these additional parameters are required:

Parameter Type Description
dzuuid string Unique upload identifier
dzchunkindex integer Current chunk index (0-based)
dztotalfilesize integer Total file size in bytes
dzchunksize integer Size of each chunk in bytes
dztotalchunkcount integer Total number of chunks
dzchunkbyteoffset integer Byte offset of current chunk

List files

GET /api/v1/files

Retrieve a list of your uploaded files.

Query Parameters

Parameter Type Description
page integer Page number (default: 1)
per_page integer Items per page (1-100, default: 20)
type string Filter by type: image, video, audio, document, archive, other, file, pdf
sort string Sort by: name, size, created_at, downloads (default: created_at)
order string Sort order: asc, desc (default: desc)

Response

{
  "success": true,
  "data": [
    {
      "id": "abc123def456",
      "name": "backup.zip",
      "size": 1048576,
      "type": "file",
      "extension": "zip",
      "downloads": 5,
      "views": 10,
      "checksum": "sha256_hash",
      "created_at": "2024-01-15T10:30:00Z",
      "expires_at": "2024-02-15T10:30:00Z",
      "download_url": "https://manastorage.com/download/abc123def456"
    }
  ],
  "pagination": {
    "total": 50,
    "per_page": 20,
    "current_page": 1,
    "last_page": 3,
    "from": 1,
    "to": 20
  }
}

Download file

GET /api/v1/files/{id}

Get download URL for a specific file.

Path Parameters

Parameter Type Description
id string The file ID

Response

{
  "success": true,
  "data": {
    "download_url": "https://temporary-signed-url.com/backup.zip",
    "expires_in": 3600,
    "filename": "backup.zip",
    "size": 1048576,
    "type": "file"
  }
}

File metadata

HEAD /api/v1/files/{id}

Get metadata for a specific file without downloading it.

Path Parameters

Parameter Type Description
id string The file ID

Response

{
  "success": true,
  "data": {
    "id": "abc123def456",
    "name": "backup.zip",
    "size": 1048576,
    "type": "file",
    "mime": "application/zip",
    "extension": "zip",
    "checksum": "sha256_hash",
    "downloads": 5,
    "views": 10,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z",
    "expires_at": "2024-02-15T10:30:00Z"
  }
}

Delete file

DELETE /api/v1/files/{id}

Permanently delete a file from your storage.

Path Parameters

Parameter Type Description
id string The file ID

Response

{
  "success": true,
  "message": "File deleted successfully"
}

Rate Limits

API requests are rate-limited per API key based on your subscription plan and key type:

Key Type / Plan Requests per Minute Key Prefix
Test Mode 2 MS_test_*
Adept Monthly 10 MS_live_*
Adept Yearly 10 MS_live_*
Adept Lifetime 10 MS_live_*
Archmage Monthly 20 MS_live_*
Archmage Yearly 20 MS_live_*
Archmage Lifetime 20 MS_live_*
Elder Sage Monthly 30 MS_live_*
Elder Sage Yearly 30 MS_live_*
Elder Sage Lifetime 30 MS_live_*
Test vs Production Keys
  • Test Keys (MS_test_*): Lower rate limits for development and testing. Functionally identical to production keys but with reduced rate limits.
  • Production Keys (MS_live_*): Full rate limits based on your subscription plan. Use for live applications.

Rate limit information is included in all API response headers:

  • X-RateLimit-Limit - Maximum requests allowed per minute
  • X-RateLimit-Remaining - Remaining requests in current window
  • X-RateLimit-Reset - Unix timestamp when the rate limit window resets
  • Retry-After - Seconds to wait before retrying (only on 429 responses)

When you exceed the rate limit, you'll receive a 429 Too Many Requests response:

{
    "error": true,
    "message": "API rate limit exceeded. Please try again later.",
    "error_code": "API_008"
}

Error Codes

The API returns standard HTTP status codes and custom error codes:

Code Status Description
API_001 401 Invalid API key
API_004 401 Invalid request signature
API_008 429 Rate limit exceeded
API_009 507 Storage quota exceeded
API_010 413 File size exceeds limit
API_011 413 Chunk size exceeds 90MB limit

Webhooks

Webhook support is coming soon. This will allow you to receive notifications when files are uploaded, downloaded, or deleted.

Code Examples

Complete examples showing how to authenticate and use the API in various programming languages.

Python Examples

1. Constructing HMAC Signature and Testing with List Files

This example shows how to construct the HMAC signature for authentication and test it with the list files endpoint.

import requests
import hmac
import hashlib
import base64
import time
import uuid

class ManaStorageAuth:
    def __init__(self, api_key_id, api_secret):
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
    
    def generate_signature(self, method, path, body=''):
        """Generate HMAC-SHA256 signature for API authentication"""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        # Calculate body hash (SHA256 hex digest)
        body_hash = ''
        if body:
            body_hash = hashlib.sha256(body.encode()).hexdigest()
        
        # Create canonical string
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        # Generate HMAC signature
        signature = hmac.new(
            self.api_secret.encode(),
            canonical.encode(),
            hashlib.sha256
        ).digest()
        
        # Base64 encode the signature
        encoded_signature = base64.b64encode(signature).decode()
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def test_with_list_files(self):
        """Test authentication by listing files"""
        path = '/api/v1/files'
        headers = self.generate_signature('GET', path)
        
        response = requests.get(f'{self.base_url}/files', headers=headers)
        
        if response.status_code == 200:
            print("✅ Authentication successful!")
            data = response.json()
            print(f"Total files: {data.get('pagination', {}).get('total', 0)}")
            return data
        else:
            print(f"❌ Authentication failed: {response.status_code}")
            print(f"Response: {response.text}")
            return None

# Example usage
if __name__ == "__main__":
    # Replace with your actual API credentials
    API_KEY_ID = 'MS_live_your_key_id_here'
    API_SECRET = 'your_api_secret_here'
    
    # Initialize authentication
    auth = ManaStorageAuth(API_KEY_ID, API_SECRET)
    
    # Test authentication with list files
    print("Testing API authentication...")
    result = auth.test_with_list_files()
    
    if result:
        print("\nFirst few files:")
        for file in result.get('data', [])[:3]:
            print(f"- {file['name']} ({file['size']} bytes)")

2. Uploading a Small File (Less than 90MB)

This example demonstrates uploading a file that's smaller than 90MB without chunking.

import requests
import hmac
import hashlib
import base64
import time
import uuid
import os
from pathlib import Path

class ManaStorageUploader:
    def __init__(self, api_key_id, api_secret):
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
    
    def generate_signature(self, method, path, body=''):
        """Generate HMAC-SHA256 signature for API authentication"""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        # Calculate body hash (SHA256 hex digest)
        body_hash = ''
        if body:
            body_hash = hashlib.sha256(body.encode()).hexdigest()
        
        # Create canonical string
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        # Generate HMAC signature
        signature = hmac.new(
            self.api_secret.encode(),
            canonical.encode(),
            hashlib.sha256
        ).digest()
        
        # Base64 encode the signature
        encoded_signature = base64.b64encode(signature).decode()
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def upload_small_file(self, file_path, password=None, expiry_days=None, notes=None):
        """Upload a file less than 90MB without chunking"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"File not found: {file_path}")
        
        file_size = os.path.getsize(file_path)
        if file_size >= 90 * 1024 * 1024:  # 90MB
            raise ValueError("File is too large for direct upload. Use chunked upload instead.")
        
        print(f"Uploading {Path(file_path).name} ({file_size:,} bytes)...")
        
        # Prepare form data
        with open(file_path, 'rb') as f:
            files = {'file': (Path(file_path).name, f, 'application/octet-stream')}
            data = {}
            
            # Add optional parameters
            if password:
                data['password'] = password
            if expiry_days:
                data['expiry_days'] = str(expiry_days)
            if notes:
                data['notes'] = notes
            
            # Generate signature for POST request
            # Server hashes JSON of form fields (excluding file content)
            fields_to_hash = dict(data)
            import json
            json_body = json.dumps(fields_to_hash, separators=(',', ':'), ensure_ascii=True) if fields_to_hash else ''
            headers = self.generate_signature('POST', '/api/v1/files', body=json_body)
            
            # Make the upload request
            response = requests.post(
                f'{self.base_url}/files',
                headers=headers,
                files=files,
                data=data
            )
        
        if response.status_code in (200, 201):
            result = response.json()
            print(f"✅ Upload successful (HTTP {response.status_code})!")
            print(f"File ID: {result['data']['id']}")
            print(f"Download URL: {result['data']['download_url']}")
            return result
        else:
            print(f"❌ Upload failed: {response.status_code}")
            print(f"Response: {response.text}")
            return None

# Example usage
if __name__ == "__main__":
    # Replace with your actual API credentials
    API_KEY_ID = 'MS_live_your_key_id_here'
    API_SECRET = 'your_api_secret_here'

    # Initialize uploader
    uploader = ManaStorageUploader(API_KEY_ID, API_SECRET)
    
    # Upload a small file
    file_to_upload = "example_document.pdf"  # Replace with your file path
    
    try:
        result = uploader.upload_small_file(
            file_path=file_to_upload,
            password="mysecretpassword",  # Optional
            expiry_days=30,               # Optional: expires in 30 days
            notes="Important document"    # Optional: max 25 chars
        )
        
        if result:
            print(f"\nFile uploaded successfully!")
            print(f"Share URL: {result['data']['download_url']}")
            
    except FileNotFoundError as e:
        print(f"Error: {e}")
    except ValueError as e:
        print(f"Error: {e}")

3. Chunked Upload for Large Files (1GB Example)

This example shows how to upload large files using chunked upload for files like 1GB or larger.

import requests
import hmac
import hashlib
import base64
import time
import uuid
import os
import math
import json
from pathlib import Path

# --- Production-Ready API Client ---

API_KEY_ID = "MS_live_your_key_id_here"
API_SECRET = "your_api_secret_here"

class ManaStorageChunkedUploader:
    """Handles large file uploads with chunking, retries, and progress."""
    
    def __init__(self, api_key_id, api_secret, chunk_size=50*1024*1024):  # 50MB default
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
        # Server enforces a max 90MB chunk size; we'll use a safe default.
        self.chunk_size = min(chunk_size, 90 * 1024 * 1024)
    
    def generate_signature(self, method, path, body=''):
        """Generates the required HMAC-SHA256 signature for API authentication."""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        body_hash = ''
        if body:
            # The body for signature must be a compact JSON string of the form fields.
            body_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()
        
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        signature = hmac.new(
            self.api_secret.encode('utf-8'),
            canonical.encode('utf-8'),
            hashlib.sha256
        ).digest()
        
        encoded_signature = base64.b64encode(signature).decode('utf-8')
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def upload_large_file(self, file_path, password=None, expiry_days=None, notes=None, 
                          chunk_delay=0.5, progress_callback=None):
        """
        Uploads a large file using a chunked method.
        Includes retries with exponential backoff for network resilience.
        """
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"File not found: {file_path}")
        
        file_size = os.path.getsize(file_path)
        filename = Path(file_path).name
        
        total_chunks = math.ceil(file_size / self.chunk_size)
        upload_uuid = str(uuid.uuid4())
        
        print(f"Starting chunked upload of '{filename}'")
        print(f"File size: {file_size / (1024*1024):.2f} MB, Chunks: {total_chunks}")
        
        successful_chunks = 0
        
        with open(file_path, 'rb') as file:
            for chunk_index in range(total_chunks):
                file.seek(chunk_index * self.chunk_size)
                chunk_data = file.read(self.chunk_size)
                
                print(f"\nUploading chunk {chunk_index + 1}/{total_chunks}...")
                
                form_data = {
                    'dzuuid': upload_uuid,
                    'dzchunkindex': str(chunk_index),
                    'dztotalfilesize': str(file_size),
                    'dzchunksize': str(self.chunk_size),
                    'dztotalchunkcount': str(total_chunks),
                    'dzchunkbyteoffset': str(chunk_index * self.chunk_size)
                }
                
                if chunk_index == 0:
                    if password: form_data['password'] = password
                    if expiry_days: form_data['expiry_days'] = str(expiry_days)
                    if notes: form_data['notes'] = notes
                
                # For multipart uploads, the signature body is a compact, sorted JSON of form fields.
                # Sorting is critical for a deterministic hash.
                json_body_for_sig = json.dumps(dict(sorted(form_data.items())), separators=(',', ':'))

                files_payload = {'file': (filename, chunk_data, 'application/octet-stream')}
                
                # Convert form_data to a sorted list of tuples to ensure the multipart payload
                # has the same deterministic field order as the signature.
                form_data_ordered = sorted(form_data.items())

                # Retry logic for network issues or server timeouts
                max_retries = 5 if chunk_index == total_chunks - 1 else 3
                for attempt in range(max_retries):
                    # Regenerate signature for each attempt to get a fresh timestamp/nonce
                    headers = self.generate_signature('POST', '/api/v1/files', body=json_body_for_sig)
                    
                    try:
                        # Use a longer timeout for the final chunk for server-side processing
                        timeout = 600 if chunk_index == total_chunks - 1 else 300
                        response = requests.post(
                            f'{self.base_url}/files',
                            headers=headers,
                            files=files_payload,
                            data=form_data_ordered,
                            timeout=timeout
                        )
                        
                        if response.status_code in [200, 201]:
                            successful_chunks += 1
                            print(f"✅ Chunk {chunk_index + 1} uploaded successfully.")
                            if progress_callback:
                                progress = (successful_chunks / total_chunks) * 100
                                progress_callback(successful_chunks, total_chunks, progress)
                            break # Success, exit retry loop
                        
                        print(f"❌ Chunk {chunk_index + 1} failed (Attempt {attempt + 1}/{max_retries}): HTTP {response.status_code}")
                        if attempt == max_retries - 1:
                            raise Exception(f"Chunk upload failed after {max_retries} attempts: {response.text}")
                        
                        time.sleep(2 ** (attempt + 1)) # Exponential backoff: 2s, 4s, 8s...
                            
                    except requests.exceptions.RequestException as e:
                        print(f"❌ Network error on chunk {chunk_index + 1} (Attempt {attempt + 1}/{max_retries}): {e}")
                        if attempt == max_retries - 1:
                            raise
                        time.sleep(2 ** (attempt + 1))
                
                if chunk_index < total_chunks - 1:
                    time.sleep(chunk_delay)
        
        if successful_chunks == total_chunks:
            print("\n🎉 Large file upload seems complete. Verifying...")
            # The final chunk's response may not have the file info due to async processing.
            # It's more reliable to fetch the latest file's metadata.
            time.sleep(3) # Give server a moment to finalize the file
            path = '/api/v1/files?page=1&per_page=1&sort=created_at&order=desc'
            headers = self.generate_signature('GET', path)
            latest_resp = requests.get(f'{self.base_url}/files', headers=headers, params={'page': 1, 'per_page': 1, 'sort': 'created_at', 'order': 'desc'})
            
            if latest_resp.status_code == 200:
                latest_file_data = latest_resp.json()
                if latest_file_data.get('data'):
                    return latest_file_data['data'][0]
            
            print("Could not verify final file, but all chunks were sent.")
            return {"success": True, "message": "All chunks sent, but final verification failed."}
        else:
            raise Exception(f"Upload incomplete: {successful_chunks}/{total_chunks} chunks uploaded")

def simple_progress_bar(completed, total, percentage):
    """A simple textual progress bar."""
    bar_length = 50
    filled = int(bar_length * completed // total)
    bar = '█' * filled + '-' * (bar_length - filled)
    print(f"Progress: |{bar}| {percentage:.1f}% ({completed}/{total})")

# --- Example Usage ---
if __name__ == "__main__":
    uploader = ManaStorageChunkedUploader(API_KEY_ID, API_SECRET)
    
    # Create a dummy large file for testing if it doesn't exist
    large_file_path = "large_test_file.bin"
    if not os.path.exists(large_file_path):
        print(f"Creating a dummy 150MB file at '{large_file_path}' for testing...")
        with open(large_file_path, 'wb') as f:
            f.write(os.urandom(150 * 1024 * 1024)) # 150MB
    
    try:
        final_file_info = uploader.upload_large_file(
            file_path=large_file_path,
            password="super-secret-password-123",
            expiry_days=7,
            notes="This is a test",
            chunk_delay=0.5, # 500ms delay between chunks
            progress_callback=simple_progress_bar
        )
        
        if final_file_info:
            print("\n--- Upload Successful! ---")
            print(f"File ID: {final_file_info.get('id')}")
            # Construct the share URL from the file ID
            if final_file_info.get('id'):
                share_url = f"https://manastorage.com/en/{final_file_info.get('id')}/file"
                print(f"Share URL: {share_url}")
            print(f"Download URL: {final_file_info.get('download_url')}")
            
    except FileNotFoundError as e:
        print(f"\n❌ Error: {e}")
    except Exception as e:
        print(f"\n❌ An unexpected error occurred: {e}")

4. Listing Files with Pagination and Filtering

This example demonstrates how to list files with various filters and pagination options.

import requests
import hmac
import hashlib
import base64
import time
import uuid

API_KEY_ID = "MS_live_yb5WtlyA"
API_SECRET = "gsNtaC8BPkOFf2EVL1fYRFmzZCw2JIGcMf+SqhsWFEw="

class ManaStorageFileManager:
    """Handles file management operations"""
    
    def __init__(self, api_key_id, api_secret):
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
    
    def generate_signature(self, method, path, body=''):
        """Generate HMAC-SHA256 signature for API authentication"""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        # Calculate body hash (SHA256 hex digest)
        body_hash = ''
        if body:
            body_hash = hashlib.sha256(body.encode()).hexdigest()
        
        # Create canonical string
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        # Generate HMAC signature
        signature = hmac.new(
            self.api_secret.encode(),
            canonical.encode(),
            hashlib.sha256
        ).digest()
        
        # Base64 encode the signature
        encoded_signature = base64.b64encode(signature).decode()
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def list_files(self, page=1, per_page=20, file_type=None, sort_by='created_at', 
                   sort_order='desc'):
        """List files with pagination and filtering options"""
        # Build query parameters
        params = {
            'page': page,
            'per_page': min(per_page, 100)  # Max 100 per page
        }
        
        if file_type:
            params['type'] = file_type
        if sort_by:
            params['sort'] = sort_by
        if sort_order:
            params['order'] = sort_order
        
        # Build path with query string
        query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
        path = f'/api/v1/files?{query_string}'
        
        # Generate signature and make request
        headers = self.generate_signature('GET', path)
        response = requests.get(f'{self.base_url}/files', headers=headers, params=params)
        
        if response.status_code == 200:
        return response.json()
        else:
            print(f"❌ Failed to list files: {response.status_code}")
            print(f"Response: {response.text}")
            return None

# Example usage
if __name__ == "__main__":
    API_KEY_ID = "MS_live_yb5WtlyA"
    API_SECRET = "gsNtaC8BPkOFf2EVL1fYRFmzZCw2JIGcMf+SqhsWFEw="
    file_manager = ManaStorageFileManager(API_KEY_ID, API_SECRET)
    # List first page of files
    print("=== Recent Files (Page 1) ===")
    result = file_manager.list_files(page=1, per_page=10)
    if result:
        for file in result['data']:
            size_mb = file['size'] / (1024 * 1024)
            print(f"📄 {file['name']} ({size_mb:.1f}MB) - {file['downloads']} downloads")

5. Downloading Files

This example shows how to get download URLs and download files programmatically.

import requests
import hmac
import hashlib
import base64
import time
import uuid
import urllib.request
import os
from urllib.parse import urlparse

API_KEY_ID = "YourAIPKEy"
API_SECRET = "YourApiSecret"

class ManaStorageDownloader:
    """Handles file download operations"""
    
    def __init__(self, api_key_id, api_secret):
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
    
    def generate_signature(self, method, path, body=''):
        """Generate HMAC-SHA256 signature for API authentication"""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        body_hash = ''
        if body:
            body_hash = hashlib.sha256(body.encode()).hexdigest()
        
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        signature = hmac.new(
            self.api_secret.encode(),
            canonical.encode(),
            hashlib.sha256
        ).digest()
        
        encoded_signature = base64.b64encode(signature).decode()
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def get_download_url(self, file_id):
        """Get a temporary download URL for a file"""
        path_for_signature = f'/api/v1/files/{file_id}'
        headers = self.generate_signature('GET', path_for_signature)
        
        request_url = f'{self.base_url}/files/{file_id}'
        response = requests.get(request_url, headers=headers)
        
        if response.status_code == 200:
            result = response.json()
            return result.get('data')
        else:
            print(f"❌ Failed to get download URL: {response.status_code}")
            return None
    
    def download_file(self, file_id, download_path=None):
        """Download a file to local storage"""
        # Get download URL first
        download_info = self.get_download_url(file_id)
        if not download_info:
            return False
        
        download_url = download_info['download_url']
        filename = download_info['filename']
        
        # Determine download path
        if not download_path:
            download_path = filename
        
        print(f"Downloading {filename} to {download_path}")
        
        try:
            urllib.request.urlretrieve(download_url, download_path)
            print(f"✅ Download completed: {download_path}")
            return True
        except Exception as e:
            print(f"❌ Download failed: {e}")
            return False

# Example usage
if __name__ == "__main__":
    downloader = ManaStorageDownloader(API_KEY_ID, API_SECRET)
    
    # Download a single file
    file_id = "abc123def456"  # Replace with actual file ID
    success = downloader.download_file(file_id, "./downloads/")

6. Getting File Metadata

This example shows how to retrieve detailed file metadata without downloading the file.

import requests
import hmac
import hashlib
import base64
import time
import uuid

API_KEY_ID = "APIKEY"
API_SECRET = "APISECRET"

class ManaStorageMetadata:
    """Handles file metadata operations"""
    
    def __init__(self, api_key_id, api_secret):
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
    
    def generate_signature(self, method, path, body=''):
        """Generate HMAC-SHA256 signature for API authentication"""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        body_hash = ''
        if body:
            body_hash = hashlib.sha256(body.encode()).hexdigest()
        
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        signature = hmac.new(
            self.api_secret.encode(),
            canonical.encode(),
            hashlib.sha256
        ).digest()
        
        encoded_signature = base64.b64encode(signature).decode()
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def get_file_metadata(self, file_id):
        """Get detailed metadata for a specific file"""
        path_for_signature = f'/api/v1/files/{file_id}'
        
        # The HEAD request is a lightweight way to check for existence, but we need GET for the body.
        headers = self.generate_signature('GET', path_for_signature)
        request_url = f'{self.base_url}/files/{file_id}'
        response = requests.get(request_url, headers=headers)
        
        if response.status_code == 200:
            result = response.json()
            return result.get('data')
        
        print(f"❌ Failed to get metadata: {response.status_code}")
        return None

# Example usage
if __name__ == "__main__":
    metadata_client = ManaStorageMetadata(API_KEY_ID, API_SECRET)
    
    # Get metadata for a single file
    file_id = "abc123def456"  # Replace with actual file ID
    metadata = metadata_client.get_file_metadata(file_id)
    
    if metadata:
        print(f"--- Metadata for {file_id} ---")
        if 'filename' in metadata:
            print(f"📄 File: {metadata['filename']}")
        if 'size' in metadata:
            # Format size with commas for readability
            size = metadata['size']
            if isinstance(size, (int, float)):
                print(f"📏 Size: {size:,.0f} bytes")
            else:
                print(f"📏 Size: {size} bytes")

7. Deleting Files

This example shows how to safely delete files with confirmation.

import requests
import hmac
import hashlib
import base64
import time
import uuid

API_KEY_ID = "APIKey"
API_SECRET = "APISECRET"

class ManaStorageDeleter:
    """Handles file deletion operations"""
    
    def __init__(self, api_key_id, api_secret):
        self.api_key_id = api_key_id
        self.api_secret = api_secret
        self.base_url = 'https://manastorage.com/api/v1'
    
    def generate_signature(self, method, path, body=''):
        """Generate HMAC-SHA256 signature for API authentication"""
        timestamp = str(int(time.time()))
        nonce = str(uuid.uuid4())
        
        body_hash = ''
        if body:
            body_hash = hashlib.sha256(body.encode()).hexdigest()
        
        canonical = f"{method}\n{path}\n{body_hash}\n{self.api_key_id}\n{timestamp}\n{nonce}"
        
        signature = hmac.new(
            self.api_secret.encode(),
            canonical.encode(),
            hashlib.sha256
        ).digest()
        
        encoded_signature = base64.b64encode(signature).decode()
        
        return {
            'X-Api-Key': self.api_key_id,
            'X-Api-Timestamp': timestamp,
            'X-Api-Nonce': nonce,
            'X-Api-Signature': encoded_signature
        }
    
    def delete_file(self, file_id):
        """Delete a single file"""
        path_for_signature = f'/api/v1/files/{file_id}'
        headers = self.generate_signature('DELETE', path_for_signature)
        
        request_url = f'{self.base_url}/files/{file_id}'
        response = requests.delete(request_url, headers=headers)
        
        if response.status_code == 200:
            return response.json().get('success', False)
        else:
            print(f"❌ Failed to delete file: {response.status_code}")
            return False

# Example usage
if __name__ == "__main__":
    deleter = ManaStorageDeleter(API_KEY_ID, API_SECRET)
    
    # Delete a single file
    file_id = "abc123def456"  # Replace with actual file ID
    success = deleter.delete_file(file_id)

C# (.NET)

1. Constructing HMAC Signature and Testing with List Files

This example shows how to construct the HMAC signature for authentication and test it with the list files endpoint.

using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json;
using System.Collections.Generic;

public class ManaStorageAuth
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";
    
    public ManaStorageAuth(string apiKeyId, string apiSecret)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }
    
    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
        using (var sha256 = SHA256.Create())
        {
            var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            
            return new Dictionary<string, string> {
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }

    public async Task<JsonElement> TestWithListFilesAsync()
    {
        var path = "/api/v1/files";
        var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/files");
        var headers = GenerateSignature("GET", path);
        foreach (var header in headers)
            request.Headers.Add(header.Key, header.Value);

        var response = await _client.SendAsync(request);
        var content = await response.Content.ReadAsStringAsync();
        
        if (response.IsSuccessStatusCode)
        {
            Console.WriteLine("✅ Authentication successful!");
            try
            {
                return JsonSerializer.Deserialize<JsonElement>(content);
            }
            catch (JsonException)
            {
                Console.WriteLine($"Invalid JSON response: {content}");
                return JsonSerializer.Deserialize<JsonElement>("{\"error\":\"Invalid JSON response\"}");
            }
        }
        else
        {
            Console.WriteLine($"❌ Authentication failed: {(int)response.StatusCode}");
            Console.WriteLine($"Response: {content}");
            return JsonSerializer.Deserialize<JsonElement>("{\"error\":true}");
        }
    }
}

// Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var auth = new ManaStorageAuth("MS_live_your_key_id_here", "your_api_secret_here");
        var result = await auth.TestWithListFilesAsync();
        Console.WriteLine(result);
    }
}

2. Uploading a Small File (Less than 90MB)

This example demonstrates uploading a file that's smaller than 90MB without chunking.

using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;

public class ManaStorageUploader
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";

    public ManaStorageUploader(string apiKeyId, string apiSecret)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }

    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
            using (var sha256 = SHA256.Create())
            {
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            return new Dictionary<string, string>{
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }
    
    public async Task<JsonElement> UploadSmallFileAsync(string filePath, string password = null, int? expiryDays = null, string notes = null)
    {
        if (!File.Exists(filePath))
            throw new FileNotFoundException($"File not found: {filePath}");

        var fileInfo = new FileInfo(filePath);
        if (fileInfo.Length >= 90L * 1024 * 1024)
            throw new InvalidOperationException("File is too large for direct upload. Use chunked upload instead.");

        using (var form = new MultipartFormDataContent())
        {
            var fileBytes = await File.ReadAllBytesAsync(filePath);
            form.Add(new ByteArrayContent(fileBytes), "file", Path.GetFileName(filePath));
            
            var fields = new Dictionary<string, string>();
        if (!string.IsNullOrEmpty(password))
            { 
                form.Add(new StringContent(password), "password");
                fields["password"] = password; 
            }
        if (expiryDays.HasValue)
            { 
                form.Add(new StringContent(expiryDays.Value.ToString()), "expiry_days");
                fields["expiry_days"] = expiryDays.Value.ToString(); 
            }
            if (!string.IsNullOrEmpty(notes)) 
            { 
                form.Add(new StringContent(notes), "notes"); 
                fields["notes"] = notes; 
            }

            // For multipart uploads, signature is generated with JSON of form fields (excluding file)
            // Server expects JSON encoding: empty array "[]" when no fields
            var jsonBody = fields.Count > 0 
                ? JsonSerializer.Serialize(fields, new JsonSerializerOptions { WriteIndented = false }) 
                : "[]";
            var headers = GenerateSignature("POST", "/api/v1/files", jsonBody);
            
            var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/files");
            foreach (var header in headers) request.Headers.Add(header.Key, header.Value);
            request.Content = form;
            
            var response = await _client.SendAsync(request);
            var content = await response.Content.ReadAsStringAsync();
            
            if (response.IsSuccessStatusCode)
            {
                try
                {
                    return JsonSerializer.Deserialize<JsonElement>(content);
                }
                            catch (JsonException)
            {
                Console.WriteLine($"Invalid JSON response: {content}");
                return JsonSerializer.Deserialize<JsonElement>("{\"error\":\"Invalid JSON response\"}");
            }
        }
        else
        {
            Console.WriteLine($"Upload failed: {(int)response.StatusCode} - {content}");
            return JsonSerializer.Deserialize<JsonElement>("{\"error\":true}");
        }
        }
    }
}

// Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var uploader = new ManaStorageUploader("MS_live_your_key_id_here", "your_api_secret_here");
        var result = await uploader.UploadSmallFileAsync("example_document.pdf", 
            password: "mysecretpassword", expiryDays: 30, notes: "Important document");
        Console.WriteLine(result);
    }
}

3. Chunked Upload for Large Files (1GB Example)

This example shows how to upload large files using chunked upload for files like 1GB or larger.

using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;

public class ManaStorageChunkedUploader
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";
    private readonly int _chunkSize;

    public ManaStorageChunkedUploader(string apiKeyId, string apiSecret, int chunkSizeBytes = 50 * 1024 * 1024)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
        _chunkSize = Math.Min(chunkSizeBytes, 90 * 1024 * 1024);
    }

    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
            using (var sha256 = SHA256.Create())
            {
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            return new Dictionary<string, string>{
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }

    public async Task<JsonElement> UploadLargeFileAsync(string filePath, string password = null, int? expiryDays = null, string notes = null)
    {
        if (!File.Exists(filePath))
            throw new FileNotFoundException($"File not found: {filePath}");

        var fileInfo = new FileInfo(filePath);
        var totalChunks = (int)Math.Ceiling((double)fileInfo.Length / _chunkSize);
        var uploadUuid = Guid.NewGuid().ToString();

        using (var fs = File.OpenRead(filePath))
        {
            for (var chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++)
            {
                var chunkStart = (long)chunkIndex * _chunkSize;
                var currentSize = (int)Math.Min(_chunkSize, fileInfo.Length - chunkStart);
                
                Console.WriteLine($"Uploading chunk {chunkIndex + 1}/{totalChunks} ({currentSize:N0} bytes)...");
                var buffer = new byte[currentSize];
                fs.Seek(chunkStart, SeekOrigin.Begin);
                var read = await fs.ReadAsync(buffer, 0, currentSize);
                if (read != currentSize) throw new IOException("Failed to read file chunk");

                var fields = new Dictionary<string, string> {
                    { "dzuuid", uploadUuid },
                    { "dzchunkindex", chunkIndex.ToString() },
                    { "dztotalfilesize", fileInfo.Length.ToString() },
                    { "dzchunksize", _chunkSize.ToString() },
                    { "dztotalchunkcount", totalChunks.ToString() },
                    { "dzchunkbyteoffset", chunkStart.ToString() }
                };

                if (chunkIndex == 0)
                {
                    if (!string.IsNullOrEmpty(password)) fields["password"] = password;
                    if (expiryDays.HasValue) fields["expiry_days"] = expiryDays.Value.ToString();
                    if (!string.IsNullOrEmpty(notes)) fields["notes"] = notes;
                }

                // Retry logic for chunks, especially final chunk
                var maxRetries = chunkIndex == totalChunks - 1 ? 4 : 3;
                var timeout = chunkIndex == totalChunks - 1 ? TimeSpan.FromMinutes(10) : TimeSpan.FromMinutes(5);
                
                for (int attempt = 0; attempt < maxRetries; attempt++)
                {
                    try
                    {
                        // Regenerate signature for each attempt to ensure fresh timestamp/nonce
                        var jsonBody = JsonSerializer.Serialize(fields, new JsonSerializerOptions { WriteIndented = false });
                        var headers = GenerateSignature("POST", "/api/v1/files", jsonBody);

                        using (var form = new MultipartFormDataContent())
                        {
                            var fileContent = new ByteArrayContent(buffer);
                            fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
                            form.Add(fileContent, "file", Path.GetFileName(filePath));
                            foreach (var kv in fields)
                                form.Add(new StringContent(kv.Value), kv.Key);

                            var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/files");
                            foreach (var header in headers) request.Headers.Add(header.Key, header.Value);
                            request.Content = form;
                            
                            using (var cts = new CancellationTokenSource(timeout))
                            {
                                var response = await _client.SendAsync(request, cts.Token);
                                if (response.IsSuccessStatusCode)
                                {
                                    break; // Success, exit retry loop
                                }
                                
                                var err = await response.Content.ReadAsStringAsync();
                                if (attempt == maxRetries - 1)
                                {
                                    throw new Exception($"Chunk {chunkIndex + 1}/{totalChunks} failed after {maxRetries} attempts: {(int)response.StatusCode} - {err}");
                                }
                                
                                Console.WriteLine($"Chunk {chunkIndex + 1}/{totalChunks} failed (attempt {attempt + 1}/{maxRetries}): {(int)response.StatusCode}");
                            }
                        }
                        
                        // Wait before retry (exponential backoff)
                        if (attempt < maxRetries - 1)
                        {
                            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt + 1)));
                        }
                    }
                    catch (TaskCanceledException) when (attempt < maxRetries - 1)
                    {
                        // Timeout occurred, retry if not final attempt
                        Console.WriteLine($"Chunk {chunkIndex + 1}/{totalChunks} timed out, retrying... (attempt {attempt + 1}/{maxRetries})");
                        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt + 1)));
                    }
                }
            }
        }

        // Optionally fetch most recent file
        var listPath = "/api/v1/files?page=1&per_page=1&sort=created_at&order=desc";
        var listReq = new HttpRequestMessage(HttpMethod.Get, "/api/v1/files?page=1&per_page=1&sort=created_at&order=desc");
        var listHeaders = GenerateSignature("GET", listPath);
        foreach (var h in listHeaders) listReq.Headers.Add(h.Key, h.Value);
        var listResp = await _client.SendAsync(listReq);
        var listJson = await listResp.Content.ReadAsStringAsync();
        
        if (listResp.IsSuccessStatusCode)
        {
            try
            {
                return JsonSerializer.Deserialize<JsonElement>(listJson);
            }
            catch (JsonException)
            {
                Console.WriteLine($"Invalid JSON response: {listJson}");
                return JsonSerializer.Deserialize<JsonElement>("{\"error\":\"Invalid JSON response\"}");
            }
        }
        else
        {
            Console.WriteLine($"Request failed: {(int)listResp.StatusCode} - {listJson}");
            return JsonSerializer.Deserialize<JsonElement>("{\"error\":true}");
        }
    }
}

// Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var uploader = new ManaStorageChunkedUploader("MS_live_your_key_id_here", "your_api_secret_here", 50 * 1024 * 1024);
        var result = await uploader.UploadLargeFileAsync("large_backup.zip", 
            password: "secure123", expiryDays: 7, notes: "Large backup file");
        Console.WriteLine(result);
    }
}

4. Listing Files with Pagination and Filtering

This example demonstrates how to list files with various filters and pagination options.

using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;

public class ManaStorageFileManager
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";

    public ManaStorageFileManager(string apiKeyId, string apiSecret)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }

    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
            using (var sha256 = SHA256.Create())
            {
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            return new Dictionary<string, string>{
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }

    public async Task<JsonElement> ListFilesAsync(int page = 1, int perPage = 20, string fileType = null, string sortBy = "created_at", string sortOrder = "desc")
    {
        var query = new List<string> { $"page={page}", $"per_page={Math.Min(perPage, 100)}" };
        if (!string.IsNullOrEmpty(fileType)) query.Add($"type={fileType}");
        if (!string.IsNullOrEmpty(sortBy)) query.Add($"sort={sortBy}");
        if (!string.IsNullOrEmpty(sortOrder)) query.Add($"order={sortOrder}");
        var queryString = string.Join("&", query);

        var path = $"/api/v1/files?{queryString}";
        var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/files?{queryString}");
        var headers = GenerateSignature("GET", path);
        foreach (var h in headers) request.Headers.Add(h.Key, h.Value);

        var resp = await _client.SendAsync(request);
        var json = await resp.Content.ReadAsStringAsync();
        
        if (resp.IsSuccessStatusCode)
        {
            try
            {
                return JsonSerializer.Deserialize<JsonElement>(json);
            }
            catch (JsonException)
            {
                Console.WriteLine($"Invalid JSON response: {json}");
                return JsonSerializer.Deserialize<JsonElement>("{\"error\":\"Invalid JSON response\"}");
            }
        }
        else
        {
            Console.WriteLine($"Request failed: {(int)resp.StatusCode} - {json}");
            return JsonSerializer.Deserialize<JsonElement>("{\"error\":true}");
        }
    }
}

// Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var fm = new ManaStorageFileManager("MS_live_your_key_id_here", "your_api_secret_here");
        var result = await fm.ListFilesAsync(page: 1, perPage: 10, fileType: "document");
        Console.WriteLine(result);
    }
}

5. Downloading Files

This example shows how to get download URLs and download files programmatically.

using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;

public class ManaStorageDownloader
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";

    public ManaStorageDownloader(string apiKeyId, string apiSecret)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }

    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
            using (var sha256 = SHA256.Create())
            {
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            return new Dictionary<string, string>{
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }

    public async Task<JsonElement> GetDownloadInfoAsync(string fileId)
    {
        var path = $"/api/v1/files/{fileId}";
        var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/files/{fileId}");
        var headers = GenerateSignature("GET", path);
        foreach (var h in headers) request.Headers.Add(h.Key, h.Value);
        var resp = await _client.SendAsync(request);
        var json = await resp.Content.ReadAsStringAsync();
        
        if (resp.IsSuccessStatusCode)
        {
            try
            {
                return JsonSerializer.Deserialize<JsonElement>(json);
            }
            catch (JsonException)
            {
                Console.WriteLine($"Invalid JSON response: {json}");
                return JsonSerializer.Deserialize<JsonElement>("{\"error\":\"Invalid JSON response\"}");
            }
        }
        else
        {
            Console.WriteLine($"Request failed: {(int)resp.StatusCode} - {json}");
            return JsonSerializer.Deserialize<JsonElement>("{\"error\":true}");
        }
    }

    public async Task<bool> DownloadFileAsync(string fileId, string destinationPath)
    {
        var info = await GetDownloadInfoAsync(fileId);
        
        // Check if we got valid response
        if (info.TryGetProperty("data", out var dataElement))
        {
            if (dataElement.TryGetProperty("download_url", out var urlElement))
            {
                var url = urlElement.GetString();
                Console.WriteLine($"Downloading from: {url}");
                
                try
                {
                    using (var http = new HttpClient())
                    {
                        http.Timeout = TimeSpan.FromMinutes(10);
                        
                        using (var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
                        {
                            response.EnsureSuccessStatusCode();
                            
                            // Get file size for progress
                            var contentLength = response.Content.Headers.ContentLength;
                            if (contentLength.HasValue)
                            {
                                Console.WriteLine($"File size: {contentLength.Value / 1024.0 / 1024.0:F2} MB");
                            }
                            
                            using (var stream = await response.Content.ReadAsStreamAsync())
                            using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough))
                            {
                                var buffer = new byte[4096];
                                var totalBytes = 0L;
                                int bytesRead;
                                
                                while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                                {
                                    await fs.WriteAsync(buffer, 0, bytesRead);
                                    await fs.FlushAsync(); // Force immediate write to disk
                                    
                                    totalBytes += bytesRead;
                                    
                                    // Progress every 50KB for more frequent updates
                                    if (totalBytes % (50 * 1024) == 0)
                                    {
                                        var progress = contentLength.HasValue 
                                            ? $"{(double)totalBytes / contentLength.Value * 100:F1}%" 
                                            : $"{totalBytes / 1024.0:F1} KB";
                                        Console.WriteLine($"Downloaded: {progress}");
                                    }
                                }
                                
                                await fs.FlushAsync(); // Final flush
                            }
                        }
                        
                        Console.WriteLine($"✅ Download completed: {destinationPath}");
                        return true;
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"❌ Download error: {ex.Message}");
                    return false;
                }
            }
            else
            {
                Console.WriteLine("❌ No download_url found in response");
            }
        }
        else
        {
            Console.WriteLine("❌ No data property found in response");
        }
        
        Console.WriteLine($"Failed to get download URL: {info}");
        return false;
    }
}

// Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var dl = new ManaStorageDownloader("MS_live_your_key_id_here", "your_api_secret_here");
        var success = await dl.DownloadFileAsync("abc123def456", "./downloads/document.pdf");
        Console.WriteLine(success ? "Download completed" : "Download failed");
    }
}

6. Getting File Metadata

This example shows how to retrieve detailed file metadata without downloading the file.

using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;

public class ManaStorageMetadata
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";

    public ManaStorageMetadata(string apiKeyId, string apiSecret)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }

    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
            using (var sha256 = SHA256.Create())
            {
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            return new Dictionary<string, string>{
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }

    public async Task<JsonElement> GetFileMetadataAsync(string fileId)
    {
        var path = $"/api/v1/files/{fileId}";

        // HEAD request first
        var headReq = new HttpRequestMessage(HttpMethod.Head, $"/api/v1/files/{fileId}");
        var headHeaders = GenerateSignature("HEAD", path);
        foreach (var h in headHeaders) headReq.Headers.Add(h.Key, h.Value);
        var headResp = await _client.SendAsync(headReq);

        // Follow with GET to retrieve JSON body
        var getReq = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/files/{fileId}");
        var getHeaders = GenerateSignature("GET", path);
        foreach (var h in getHeaders) getReq.Headers.Add(h.Key, h.Value);
        var getResp = await _client.SendAsync(getReq);
        var json = await getResp.Content.ReadAsStringAsync();
        
        if (getResp.IsSuccessStatusCode)
        {
            try
            {
                return JsonSerializer.Deserialize<JsonElement>(json);
            }
            catch (JsonException)
            {
                Console.WriteLine($"Invalid JSON response: {json}");
                return JsonSerializer.Deserialize<JsonElement>("{\"error\":\"Invalid JSON response\"}");
            }
        }
        else
        {
            Console.WriteLine($"Request failed: {(int)getResp.StatusCode} - {json}");
            return JsonSerializer.Deserialize<JsonElement>("{\"error\":true}");
        }
    }
    }
    
    // Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var md = new ManaStorageMetadata("MS_live_your_key_id_here", "your_api_secret_here");
        var meta = await md.GetFileMetadataAsync("abc123def456");
        Console.WriteLine(meta);
    }
}

7. Deleting Files

This example shows how to safely delete files with confirmation.

using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text.Json;

public class ManaStorageDeleter
{
    private readonly string _apiKeyId;
    private readonly string _apiSecret;
    private readonly HttpClient _client;
    private const string BaseUrl = "https://manastorage.com/api/v1";

    public ManaStorageDeleter(string apiKeyId, string apiSecret)
    {
        _apiKeyId = apiKeyId;
        _apiSecret = apiSecret;
        _client = new HttpClient { BaseAddress = new Uri(BaseUrl) };
    }

    private Dictionary<string, string> GenerateSignature(string method, string path, string body = "")
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = Guid.NewGuid().ToString();
        // Calculate body hash (matches server logic)
        string bodyHash = string.Empty;
        if (!string.IsNullOrEmpty(body))
        {
            using (var sha256 = SHA256.Create())
            {
                var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
                bodyHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }
        }
        var canonical = $"{method}\n{path}\n{bodyHash}\n{_apiKeyId}\n{timestamp}\n{nonce}";
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret)))
        {
            var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
            var encoded = Convert.ToBase64String(signature);
            return new Dictionary<string, string>{
                { "X-Api-Key", _apiKeyId },
                { "X-Api-Timestamp", timestamp },
                { "X-Api-Nonce", nonce },
                { "X-Api-Signature", encoded }
            };
        }
    }

    public async Task<bool> DeleteFileAsync(string fileId)
    {
        var path = $"/api/v1/files/{fileId}";
        var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/files/{fileId}");
        var headers = GenerateSignature("DELETE", path);
        foreach (var h in headers) request.Headers.Add(h.Key, h.Value);
        var resp = await _client.SendAsync(request);
        return resp.IsSuccessStatusCode;
    }
}

// Example usage
class Program
{
    static async Task Main(string[] args)
    {
        var deleter = new ManaStorageDeleter("MS_live_your_key_id_here", "your_api_secret_here");
        var ok = await deleter.DeleteFileAsync("abc123def456");
        Console.WriteLine(ok ? "Deleted" : "Failed");
    }
}

Go

1. Constructing HMAC Signature and Testing with List Files

This example shows how to construct the HMAC signature for authentication and test it with the list files endpoint.

package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
	"strconv"
	"time"
	"github.com/gofrs/uuid/v5"
)

type ManaStorageAuth struct {
	APIKeyID  string
	APISecret string
	BaseURL   string
}

func NewManaStorageAuth(apiKeyID, apiSecret string) *ManaStorageAuth {
	return &ManaStorageAuth{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
	}
}

func (a *ManaStorageAuth) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	u, _ := uuid.NewV4()
	nonce := u.String()
	
	// Calculate body hash
	bodyHash := ""
	if len(body) > 0 {
		hash := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(hash[:])
	}
	
	// Create canonical string
	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
		method, path, bodyHash, a.APIKeyID, timestamp, nonce)
	
	// Generate HMAC signature
	h := hmac.New(sha256.New, []byte(a.APISecret))
	h.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
	
	return map[string]string{
		"X-Api-Key":       a.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

func (a *ManaStorageAuth) UploadFile(filePath string, password string, expiryDays int, notes string) (map[string]interface{}, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	
	// Create multipart form
	var b bytes.Buffer
	w := multipart.NewWriter(&b)
	
	// Add file
	fw, err := w.CreateFormFile("file", file.Name())
	if err != nil {
		return nil, err
	}
	if _, err := io.Copy(fw, file); err != nil {
		return nil, err
	}
	
	// Add optional fields
	if password != "" {
		w.WriteField("password", password)
	}
	if expiryDays > 0 {
		w.WriteField("expiry_days", strconv.Itoa(expiryDays))
	}
	if notes != "" {
		w.WriteField("notes", notes)
	}
	
	w.Close()
	
	// For multipart signature: hash JSON of form fields (excluding file). When no fields, use []
	fields := map[string]string{}
	if password != "" {
		fields["password"] = password
	}
	if expiryDays > 0 {
		fields["expiry_days"] = strconv.Itoa(expiryDays)
	}
	if notes != "" {
		fields["notes"] = notes
	}
	var bodyForHash []byte
	if len(fields) > 0 {
		bodyForHash, _ = json.Marshal(fields)
	} else {
		bodyForHash = []byte("[]")
	}
	
	// Create request
	req, err := http.NewRequest("POST", a.BaseURL+"/files", &b)
	if err != nil {
		return nil, err
	}
	
	// Add headers
	headers := a.generateSignature("POST", "/api/v1/files", bodyForHash)
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	req.Header.Set("Content-Type", w.FormDataContentType())
	
	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	
	// Parse response
	body, _ := ioutil.ReadAll(resp.Body)
	var result map[string]interface{}
	json.Unmarshal(body, &result)
	
	return result, nil
}

func (a *ManaStorageAuth) ListFiles(page int, perPage int, fileType string) (map[string]interface{}, error) {
	path := fmt.Sprintf("/api/v1/files?page=%d&per_page=%d", page, perPage)
	if fileType != "" {
		path += "&type=" + fileType
	}
	
	// Build URL to exactly match the signed path
	url := fmt.Sprintf("%s/files?page=%d&per_page=%d", a.BaseURL, page, perPage)
	if fileType != "" {
		url += "&type=" + fileType
	}
	
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	
	headers := a.generateSignature("GET", path, nil)
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	
	body, _ := ioutil.ReadAll(resp.Body)
	var result map[string]interface{}
	json.Unmarshal(body, &result)
	
	return result, nil
	}
	
func (a *ManaStorageAuth) DownloadFile(fileID string) (map[string]interface{}, error) {
	path := fmt.Sprintf("/api/v1/files/%s", fileID)
		
	req, err := http.NewRequest("GET", a.BaseURL+"/files/"+fileID, nil)
		if err != nil {
			return nil, err
		}
		
	headers := a.generateSignature("GET", path, nil)
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	
	body, _ := ioutil.ReadAll(resp.Body)
	var result map[string]interface{}
	json.Unmarshal(body, &result)
	
	return result, nil
}

func (a *ManaStorageAuth) DeleteFile(fileID string) (map[string]interface{}, error) {
	path := fmt.Sprintf("/api/v1/files/%s", fileID)
	
	req, err := http.NewRequest("DELETE", a.BaseURL+"/files/"+fileID, nil)
	if err != nil {
		return nil, err
	}
	
	headers := a.generateSignature("DELETE", path, nil)
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	
	body, _ := ioutil.ReadAll(resp.Body)
	var result map[string]interface{}
	json.Unmarshal(body, &result)
	
	return result, nil
}



func main() {
	api := NewManaStorageAuth("MS_live_your_key_id", "your_api_secret_here")
	
	// Test authentication by listing files
	files, err := api.ListFiles(1, 10, "")
	if err != nil {
		panic(err)
	}
	fmt.Printf("Files: %v\n", files)
}

2. Uploading a Small File (Less than 90MB)

This example demonstrates uploading a file that's smaller than 90MB without chunking.

package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"time"
	"github.com/gofrs/uuid/v5"
)

type GoSmallUploader struct {
	APIKeyID  string
	APISecret string
	BaseURL   string
}

func NewGoSmallUploader(apiKeyID, apiSecret string) *GoSmallUploader {
	return &GoSmallUploader{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
	}
}

func (u *GoSmallUploader) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonceUUID, _ := uuid.NewV4()
	nonce := nonceUUID.String()

	bodyHash := ""
	if len(body) > 0 {
		sum := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(sum[:])
	}

	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, bodyHash, u.APIKeyID, timestamp, nonce)
	mac := hmac.New(sha256.New, []byte(u.APISecret))
	mac.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	return map[string]string{
		"X-Api-Key":       u.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

func (u *GoSmallUploader) UploadSmallFile(filePath string, password string, expiryDays *int, notes string) (map[string]interface{}, error) {
	info, err := os.Stat(filePath)
	if err != nil {
		return nil, fmt.Errorf("stat failed: %w", err)
	}
	if info.Size() >= 90*1024*1024 {
		return nil, fmt.Errorf("file too large for direct upload; use chunked upload")
	}
	
	f, err := os.Open(filePath)
	if err != nil {
		return nil, fmt.Errorf("open failed: %w", err)
	}
	defer f.Close()

	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)

	// file part
	fw, err := w.CreateFormFile("file", filepath.Base(filePath))
	if err != nil {
		return nil, err
	}
	if _, err := io.Copy(fw, f); err != nil {
		return nil, err
	}

	// optional fields
	fields := map[string]string{}
	if password != "" {
		w.WriteField("password", password)
		fields["password"] = password
	}
	if expiryDays != nil {
		v := strconv.Itoa(*expiryDays)
		w.WriteField("expiry_days", v)
		fields["expiry_days"] = v
	}
	if notes != "" {
		w.WriteField("notes", notes)
		fields["notes"] = notes
	}

	if err := w.Close(); err != nil {
		return nil, err
	}

	// signature body: JSON of fields (exclude file). When none, use []
	var bodyForHash []byte
	if len(fields) > 0 {
		bodyForHash, _ = json.Marshal(fields)
	} else {
		bodyForHash = []byte("[]")
	}

	req, err := http.NewRequest("POST", u.BaseURL+"/files", &buf)
	if err != nil {
		return nil, err
	}
	for k, v := range u.generateSignature("POST", "/api/v1/files", bodyForHash) {
		req.Header.Set(k, v)
	}
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var out map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return out, nil
}

func main() {
	client := NewGoSmallUploader("MS_live_your_key_id", "your_api_secret_here")
	// Example: omit optional fields
	result, err := client.UploadSmallFile("example_document.pdf", "", nil, "")
	if err != nil {
		panic(err)
	}
	fmt.Printf("Upload result: %v\n", result)
}

3. Chunked Upload for Large Files (1GB Example)

This example shows how to upload large files using chunked upload for files like 1GB or larger.

package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"
	"github.com/google/uuid"
)

type GoChunkedUploader struct {
	APIKeyID    string
	APISecret   string
	BaseURL     string
	ChunkSize   int64
	Client      *http.Client
}

type UploadProgress struct {
	ChunkIndex      int
	TotalChunks     int
	BytesUploaded   int64
	TotalBytes      int64
	Percentage      float64
}

type ProgressCallback func(progress UploadProgress)

func NewGoChunkedUploader(apiKeyID, apiSecret string, chunkSize int64) *GoChunkedUploader {
	// Server enforces max 90MB chunk size
	if chunkSize > 90*1024*1024 {
		chunkSize = 90 * 1024 * 1024
	}
	if chunkSize <= 0 {
		chunkSize = 50 * 1024 * 1024 // Default 50MB
	}
	
	return &GoChunkedUploader{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
		ChunkSize: chunkSize,
		Client: &http.Client{
			Timeout: 10 * time.Minute, // Extended timeout for large uploads
		},
	}
}

func (u *GoChunkedUploader) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonce := uuid.New().String()
	
	var bodyHash string
	if len(body) > 0 {
		sum := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(sum[:])
	}

	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, bodyHash, u.APIKeyID, timestamp, nonce)
	mac := hmac.New(sha256.New, []byte(u.APISecret))
	mac.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	return map[string]string{
		"X-Api-Key":       u.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

// canonicalJSON creates a deterministic, key-sorted JSON string from a map,
// which is required for consistent signature generation.
func canonicalJSON(fields map[string]string) ([]byte, error) {
	if len(fields) == 0 {
		// As per other client implementations, the server expects an empty array `[]` for empty fields.
		return []byte("[]"), nil
	}

	keys := make([]string, 0, len(fields))
	for k := range fields {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var b strings.Builder
	b.WriteString("{")
	for i, k := range keys {
		if i > 0 {
			b.WriteString(",")
		}
		// Marshal key and value to get proper JSON string encoding (e.g., escaping quotes)
		keyJSON, _ := json.Marshal(k)
		valueJSON, _ := json.Marshal(fields[k])
		b.Write(keyJSON)
		b.WriteString(":")
		b.Write(valueJSON)
	}
	b.WriteString("}")

	return []byte(b.String()), nil
}

func (u *GoChunkedUploader) UploadLargeFile(filePath string, password string, expiryDays *int, notes string, chunkDelay time.Duration, progressCallback ProgressCallback) (map[string]interface{}, error) {
	// Check if file exists
	info, err := os.Stat(filePath)
	if err != nil {
		return nil, fmt.Errorf("file not found: %w", err)
	}
	
	fileSize := info.Size()
	filename := filepath.Base(filePath)
	
	// Calculate chunk information
	totalChunks := int(math.Ceil(float64(fileSize) / float64(u.ChunkSize)))
	uploadUUID := uuid.New().String()
	
	fmt.Printf("Starting chunked upload of %s\n", filename)
	fmt.Printf("File size: %d bytes (%.2f GB)\n", fileSize, float64(fileSize)/(1024*1024*1024))
	fmt.Printf("Chunk size: %d bytes\n", u.ChunkSize)
	fmt.Printf("Total chunks: %d\n", totalChunks)
	fmt.Printf("Upload UUID: %s\n", uploadUUID)
	
	file, err := os.Open(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()
	
	successfulChunks := 0
	
	for chunkIndex := 0; chunkIndex < totalChunks; chunkIndex++ {
		chunkStart := int64(chunkIndex) * u.ChunkSize
		chunkEnd := chunkStart + u.ChunkSize
		if chunkEnd > fileSize {
			chunkEnd = fileSize
		}
		currentChunkSize := chunkEnd - chunkStart
		
		fmt.Printf("\nUploading chunk %d/%d (%d bytes)...\n", chunkIndex+1, totalChunks, currentChunkSize)
		
		// Read chunk data
		chunkData := make([]byte, currentChunkSize)
		file.Seek(chunkStart, io.SeekStart)
		bytesRead, err := file.Read(chunkData)
		if err != nil || int64(bytesRead) != currentChunkSize {
			return nil, fmt.Errorf("failed to read chunk %d: %w", chunkIndex, err)
		}
		
		// Prepare chunk fields
		fields := map[string]string{
			"dzuuid":            uploadUUID,
			"dzchunkindex":      strconv.Itoa(chunkIndex),
			"dztotalfilesize":   strconv.FormatInt(fileSize, 10),
			"dzchunksize":       strconv.FormatInt(u.ChunkSize, 10),
			"dztotalchunkcount": strconv.Itoa(totalChunks),
			"dzchunkbyteoffset": strconv.FormatInt(chunkStart, 10),
		}
		
		// Add optional parameters (only for first chunk)
		if chunkIndex == 0 {
			if password != "" {
				fields["password"] = password
			}
			if expiryDays != nil {
				fields["expiry_days"] = strconv.Itoa(*expiryDays)
			}
			if notes != "" {
				fields["notes"] = notes
			}
		}
		
		// Retry logic with exponential backoff
		maxRetries := 3
		if chunkIndex == totalChunks-1 { // Final chunk gets more retries
			maxRetries = 4
		}
		
		var lastErr error
		for attempt := 0; attempt < maxRetries; attempt++ {
			// Create multipart form
			var buf bytes.Buffer
			writer := multipart.NewWriter(&buf)
			
			// Add file part
			fileWriter, err := writer.CreateFormFile("file", filename)
			if err != nil {
				return nil, fmt.Errorf("failed to create form file: %w", err)
			}
			fileWriter.Write(chunkData)
			
			// Add form fields in a deterministic (sorted) order. This is crucial for the server
			// to reconstruct the data in the same order for signature verification.
			keys := make([]string, 0, len(fields))
			for k := range fields {
				keys = append(keys, k)
			}
			sort.Strings(keys)
			for _, key := range keys {
				writer.WriteField(key, fields[key])
			}
			writer.Close()
			
			// Generate signature from a canonical (key-sorted) JSON of the fields.
			// This is critical to ensure the signature is deterministic and matches the server's calculation.
			fieldsJSON, err := canonicalJSON(fields)
			if err != nil {
				return nil, fmt.Errorf("failed to create canonical JSON for signature: %w", err)
			}
			headers := u.generateSignature("POST", "/api/v1/files", fieldsJSON)
			
			// Create request
			req, err := http.NewRequest("POST", u.BaseURL+"/files", &buf)
			if err != nil {
				return nil, fmt.Errorf("failed to create request: %w", err)
			}
			
			// Set headers
			for key, value := range headers {
				req.Header.Set(key, value)
			}
			req.Header.Set("Content-Type", writer.FormDataContentType())
			
			// Set timeout based on chunk (longer for final chunk)
			timeout := 5 * time.Minute
			if chunkIndex == totalChunks-1 {
				timeout = 10 * time.Minute
			}
			client := &http.Client{Timeout: timeout}
			
			// Make request
			resp, err := client.Do(req)
			if err != nil {
				lastErr = fmt.Errorf("network error on chunk %d (attempt %d): %w", chunkIndex+1, attempt+1, err)
				if attempt < maxRetries-1 {
					waitTime := time.Duration(math.Pow(2, float64(attempt+1))) * time.Second
					fmt.Printf("❌ %v, retrying in %v...\n", lastErr, waitTime)
					time.Sleep(waitTime)
					continue
				}
				break
			}
			
			if resp.StatusCode == 200 || resp.StatusCode == 201 {
				successfulChunks++
				fmt.Printf("✅ Chunk %d uploaded successfully (HTTP %d)\n", chunkIndex+1, resp.StatusCode)
				
				// Call progress callback if provided
				if progressCallback != nil {
					progress := UploadProgress{
						ChunkIndex:    chunkIndex + 1,
						TotalChunks:   totalChunks,
						BytesUploaded: int64(successfulChunks) * u.ChunkSize,
						TotalBytes:    fileSize,
						Percentage:    (float64(successfulChunks) / float64(totalChunks)) * 100,
					}
					progressCallback(progress)
				}
				
				// Store final response for return
				if chunkIndex == totalChunks-1 {
					var result map[string]interface{}
					if err := json.NewDecoder(resp.Body).Decode(&result); err == nil {
						resp.Body.Close()
						
						// Check if we got file info in the response
						if _, ok := result["data"]; ok {
							fmt.Printf("\n🎉 Large file upload completed successfully!\n")
							fmt.Printf("Total chunks uploaded: %d/%d\n", successfulChunks, totalChunks)
							return result, nil
						}
					} else {
						resp.Body.Close()
					}
					
					// Fallback: fetch the most recently created file, as the final chunk
					// response may not contain the completed file's metadata.
					fmt.Println("Final response had no file info, fetching latest file...")
					return u.fetchLatestFile()
				}
				
				resp.Body.Close()
				break
			} else {
				body, _ := io.ReadAll(resp.Body)
				resp.Body.Close()
				lastErr = fmt.Errorf("chunk %d failed: HTTP %d - %s", chunkIndex+1, resp.StatusCode, string(body))
				
				if attempt < maxRetries-1 {
					waitTime := time.Duration(math.Pow(2, float64(attempt+1))) * time.Second
					fmt.Printf("❌ %v, retrying in %v...\n", lastErr, waitTime)
					time.Sleep(waitTime)
					continue
				}
				break
			}
		}
		
		if lastErr != nil {
			return nil, fmt.Errorf("chunk upload failed after %d attempts: %w", maxRetries, lastErr)
		}
		
		// Delay between chunks to avoid overwhelming the server
		if chunkIndex < totalChunks-1 && chunkDelay > 0 {
			time.Sleep(chunkDelay)
		}
	}
	
	if successfulChunks != totalChunks {
		return nil, fmt.Errorf("upload incomplete: %d/%d chunks uploaded", successfulChunks, totalChunks)
	}
	
	// This part should ideally not be reached if the last chunk logic is correct,
	// but serves as a final fallback.
	fmt.Printf("\n🎉 Large file upload completed successfully!\n")
	fmt.Printf("Total chunks uploaded: %d/%d\n", successfulChunks, totalChunks)
	
	return u.fetchLatestFile()
}

func (u *GoChunkedUploader) fetchLatestFile() (map[string]interface{}, error) {
	// Add a delay to give the server a few seconds to finish processing the file
	// after acknowledging the final chunk. This is a pragmatic choice to reduce race conditions.
	fmt.Println("Waiting 3 seconds for server to finalize processing...")
	time.Sleep(3 * time.Second)

	path := "/api/v1/files?page=1&per_page=1&sort=created_at&order=desc"
	headers := u.generateSignature("GET", path, nil)
	
	req, err := http.NewRequest("GET", u.BaseURL+"/files?page=1&per_page=1&sort=created_at&order=desc", nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	
	resp, err := u.Client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch latest file: %w", err)
	}
	defer resp.Body.Close()
	
	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("failed to fetch latest file: HTTP %d - %s", resp.StatusCode, string(body))
	}
	
	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}
	
	// The response for a list is nested, e.g., {"data": [...]}. We return the first item.
	if data, ok := result["data"]; ok {
		if files, ok := data.([]interface{}); ok && len(files) > 0 {
			if fileMap, ok := files[0].(map[string]interface{}); ok {
				// Return a structure that matches the direct upload response for consistency
				return map[string]interface{}{"success": true, "data": fileMap}, nil
			}
		}
	}
	
	return result, nil
}

// Progress callback function example
func progressCallback(progress UploadProgress) {
	barLength := 50
	filledLength := int(float64(barLength) * float64(progress.ChunkIndex) / float64(progress.TotalChunks))
	bar := strings.Repeat("█", filledLength) + strings.Repeat("-", barLength-filledLength)
	fmt.Printf("Progress: |%s| %.1f%% (%d/%d chunks)\n", bar, progress.Percentage, progress.ChunkIndex, progress.TotalChunks)
}

func main() {
	// Initialize chunked uploader with 50MB chunks
	uploader := NewGoChunkedUploader(
		"MS_live_your_key_id_here", 
		"your_api_secret_here", 
		50*1024*1024, // 50MB chunks
	)
	
	// Upload a large file (e.g., 1GB)
	largeFilePath := "large_backup.zip" // Replace with your large file
	expiryDays := 7
	
	result, err := uploader.UploadLargeFile(
		largeFilePath,
		"secure123",              // password (optional)
		&expiryDays,             // expiry_days (optional)
		"Large backup file",      // notes (optional)
		500*time.Millisecond,    // 500ms delay between chunks
		progressCallback,        // progress callback function
	)
	
	if err != nil {
		fmt.Printf("Upload error: %v\n", err)
		return
	}
	
	fmt.Printf("\n🎉 Success! Large file uploaded.\n")
	if data, ok := result["data"]; ok {
		if dataMap, ok := data.(map[string]interface{}); ok {
			if fileID, ok := dataMap["id"].(string); ok {
				shareURL := fmt.Sprintf("https://manastorage.com/en/%s/file", fileID)
				fmt.Printf("Share this URL: %s\n", shareURL)
				fmt.Printf("File ID: %s\n", fileID)
			}
		} else {
			// Handle cases where the structure is unexpected
			 fmt.Printf("Full response data: %v\n", data)
		}
	}
}

4. Listing Files with Pagination and Filtering

This example demonstrates how to list files with various filters and pagination options.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/google/uuid"
)

type GoFileManager struct {
	APIKeyID  string
	APISecret string
	BaseURL   string
	Client    *http.Client
}

func NewGoFileManager(apiKeyID, apiSecret string) *GoFileManager {
	return &GoFileManager{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
		Client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (m *GoFileManager) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonce := uuid.New().String()
	
	var bodyHash string
	if len(body) > 0 {
		sum := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(sum[:])
	}

	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, bodyHash, m.APIKeyID, timestamp, nonce)
	mac := hmac.New(sha256.New, []byte(m.APISecret))
	mac.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))

	return map[string]string{
		"X-Api-Key":       m.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

func (m *GoFileManager) ListFiles(page, perPage int, fileType, sortBy, sortOrder string) (map[string]interface{}, error) {
	// Build query parameters
	params := url.Values{}
	params.Add("page", strconv.Itoa(page))
	params.Add("per_page", strconv.Itoa(perPage))
	if fileType != "" {
		params.Add("type", fileType)
	}
	if sortBy != "" {
		params.Add("sort", sortBy)
	}
	if sortOrder != "" {
		params.Add("order", sortOrder)
	}
	
	// Construct path with query string for signature generation
	path := "/api/v1/files?" + params.Encode()
	
	// Generate signature (GET request has no body)
	headers := m.generateSignature("GET", path, nil)
	
	// Create request
	req, err := http.NewRequest("GET", m.BaseURL+"/files?"+params.Encode(), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	
	for key, value := range headers {
		req.Header.Set(key, value)
	}
	
	// Make request
	resp, err := m.Client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()
	
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("API error: status code %d", resp.StatusCode)
	}
	
	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}
	
	return result, nil
}

func main() {
	fileManager := NewGoFileManager(
		"MS_live_your_key_id_here",
		"your_api_secret_here",
	)
	
	// Example: List the first page of files, 10 per page
	fmt.Println("--- Fetching recent files (Page 1) ---")
	files, err := fileManager.ListFiles(1, 10, "", "created_at", "desc")
	if err != nil {
		fmt.Printf("Error listing files: %v\n", err)
		return
	}
	
	if data, ok := files["data"]; ok {
		if fileList, ok := data.([]interface{}); ok {
			if len(fileList) == 0 {
				fmt.Println("No files found.")
			}
			for _, fileItem := range fileList {
				if fileMap, ok := fileItem.(map[string]interface{}); ok {
					sizeMB := fileMap["size"].(float64) / (1024 * 1024)
					fmt.Printf("📄 %s (%.2f MB) - %v downloads\n",
						fileMap["name"],
						sizeMB,
						fileMap["downloads"])
				}
			}
		}
	}
}

5. Downloading Files

This example shows how to get download URLs and download files programmatically.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"
)

type GoDownloader struct {
	APIKeyID  string
	APISecret string
	BaseURL   string
	Client    *http.Client
}

func NewGoDownloader(apiKeyID, apiSecret string) *GoDownloader {
	return &GoDownloader{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
		Client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (d *GoDownloader) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonce := uuid.New().String()
	var bodyHash string
	if len(body) > 0 {
		sum := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(sum[:])
	}
	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, bodyHash, d.APIKeyID, timestamp, nonce)
	mac := hmac.New(sha256.New, []byte(d.APISecret))
	mac.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	return map[string]string{
		"X-Api-Key":       d.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

// GetDownloadInfo retrieves the file metadata, including the temporary download URL.
func (d *GoDownloader) GetDownloadInfo(fileID string) (map[string]interface{}, error) {
	pathForSignature := fmt.Sprintf("/api/v1/files/%s", fileID)
	headers := d.generateSignature("GET", pathForSignature, nil)

	requestURL := fmt.Sprintf("%s/files/%s", d.BaseURL, fileID)
	req, err := http.NewRequest("GET", requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	resp, err := d.Client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("API error: status code %d", resp.StatusCode)
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	if data, ok := result["data"].(map[string]interface{}); ok {
		return data, nil
	}
	return nil, fmt.Errorf("unexpected response format: 'data' field not found or not a map")
}

// WriteCounter is a custom writer to track download progress.
type WriteCounter struct {
	Total      uint64
	Downloaded uint64
}

func (wc *WriteCounter) Write(p []byte) (int, error) {
	n := len(p)
	wc.Downloaded += uint64(n)
	wc.PrintProgress()
	return n, nil
}

func (wc *WriteCounter) PrintProgress() {
	percentage := float64(wc.Downloaded) / float64(wc.Total) * 100
	barLength := 50
	filledLength := int(float64(barLength) * percentage / 100)
	bar := strings.Repeat("█", filledLength) + strings.Repeat("-", barLength-filledLength)
	fmt.Printf("\rDownloading: |%s| %.1f%% (%d/%d bytes)", bar, percentage, wc.Downloaded, wc.Total)
}

// DownloadFile downloads a file by its ID to a specified destination path.
func (d *GoDownloader) DownloadFile(fileID, destinationPath string) error {
	fmt.Println("Getting download information...")
	info, err := d.GetDownloadInfo(fileID)
	if err != nil {
		return fmt.Errorf("could not get download info: %w", err)
	}

	downloadURL, ok := info["download_url"].(string)
	if !ok || downloadURL == "" {
		return fmt.Errorf("download URL not found in API response")
	}

	filename, ok := info["filename"].(string)
	if !ok || filename == "" {
		return fmt.Errorf("filename not found in API response")
	}

	// If destination path is a directory, append filename
	destInfo, err := os.Stat(destinationPath)
	if err == nil && destInfo.IsDir() {
		destinationPath = filepath.Join(destinationPath, filename)
	}

	fmt.Printf("Starting download of '%s' to '%s'\n", filename, destinationPath)

	// Create the destination file
	out, err := os.Create(destinationPath)
	if err != nil {
		return fmt.Errorf("failed to create destination file: %w", err)
	}
	defer out.Close()

	// Make the download request
	client := &http.Client{Timeout: 20 * time.Minute} // Longer timeout for large downloads
	resp, err := client.Get(downloadURL)
	if err != nil {
		return fmt.Errorf("failed to start download: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download failed with status: %s", resp.Status)
	}

	// Get file size for progress bar
	fileSize, _ := strconv.ParseUint(resp.Header.Get("Content-Length"), 10, 64)
	counter := &WriteCounter{Total: fileSize}

	// Copy the body to the file, passing through the progress counter
	_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
	if err != nil {
		os.Remove(destinationPath) // Attempt to remove partially downloaded file
		return fmt.Errorf("failed to write to destination file: %w", err)
	}

	fmt.Println("\n✅ Download completed successfully.")
	return nil
}

func main() {
	downloader := NewGoDownloader(
		"MS_live_your_key_id_here",
		"your_api_secret_here",
	)

	// Replace with a valid file ID from your account
	fileID := "FsDxnT5Up8YjKyd"
	destination := "./" // Download to current directory

	err := downloader.DownloadFile(fileID, destination)
	if err != nil {
		fmt.Printf("\n❌ Error downloading file: %v\n", err)
	}
}

6. Getting File Metadata

This example shows how to retrieve detailed file metadata without downloading the file.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/google/uuid"
)

type GoMetadataFetcher struct {
	APIKeyID  string
	APISecret string
	BaseURL   string
	Client    *http.Client
}

func NewGoMetadataFetcher(apiKeyID, apiSecret string) *GoMetadataFetcher {
	return &GoMetadataFetcher{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
		Client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (m *GoMetadataFetcher) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonce := uuid.New().String()
	var bodyHash string
	if len(body) > 0 {
		sum := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(sum[:])
	}
	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, bodyHash, m.APIKeyID, timestamp, nonce)
	mac := hmac.New(sha256.New, []byte(m.APISecret))
	mac.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	return map[string]string{
		"X-Api-Key":       m.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

func (m *GoMetadataFetcher) GetFileMetadata(fileID string) (map[string]interface{}, error) {
	pathForSignature := fmt.Sprintf("/api/v1/files/%s", fileID)
	headers := m.generateSignature("GET", pathForSignature, nil)

	requestURL := fmt.Sprintf("%s/files/%s", m.BaseURL, fileID)
	req, err := http.NewRequest("GET", requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	resp, err := m.Client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("API error: status code %d", resp.StatusCode)
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	if data, ok := result["data"].(map[string]interface{}); ok {
		return data, nil
	}
	return nil, fmt.Errorf("unexpected response format: 'data' field not found")
}

func main() {
	fetcher := NewGoMetadataFetcher(
		"MS_live_your_key_id_here",
		"your_api_secret_here",
	)

	// Replace with a valid file ID from your account
	fileID := "FsDxnT5Up8YjKyd"

	fmt.Printf("--- Fetching metadata for file: %s ---\n", fileID)
	metadata, err := fetcher.GetFileMetadata(fileID)
	if err != nil {
		fmt.Printf("❌ Error fetching metadata: %v\n", err)
		return
	}

	// Safely access and print metadata using the correct keys from the API response
	if filename, ok := metadata["filename"].(string); ok {
		fmt.Printf("📄 File: %s\n", filename)
	} else {
		fmt.Printf("📄 File: N/A\n")
	}
	if size, ok := metadata["size"].(float64); ok {
		fmt.Printf("📏 Size: %.0f bytes\n", size)
	} else {
		fmt.Printf("📏 Size: N/A\n")
	}
}

7. Deleting Files

This example shows how to safely delete files with confirmation.

package main

import (
	"bufio"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com.com/google/uuid"
)

type GoFileDeleter struct {
	APIKeyID  string
	APISecret string
	BaseURL   string
	Client    *http.Client
}

func NewGoFileDeleter(apiKeyID, apiSecret string) *GoFileDeleter {
	return &GoFileDeleter{
		APIKeyID:  apiKeyID,
		APISecret: apiSecret,
		BaseURL:   "https://manastorage.com/api/v1",
		Client: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (d *GoFileDeleter) generateSignature(method, path string, body []byte) map[string]string {
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	nonce := uuid.New().String()
	var bodyHash string
	if len(body) > 0 {
		sum := sha256.Sum256(body)
		bodyHash = hex.EncodeToString(sum[:])
	}
	canonical := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, bodyHash, d.APIKeyID, timestamp, nonce)
	mac := hmac.New(sha256.New, []byte(d.APISecret))
	mac.Write([]byte(canonical))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	return map[string]string{
		"X-Api-Key":       d.APIKeyID,
		"X-Api-Timestamp": timestamp,
		"X-Api-Nonce":     nonce,
		"X-Api-Signature": signature,
	}
}

func (d *GoFileDeleter) DeleteFile(fileID string) (bool, error) {
	pathForSignature := fmt.Sprintf("/api/v1/files/%s", fileID)
	headers := d.generateSignature("DELETE", pathForSignature, nil)

	requestURL := fmt.Sprintf("%s/files/%s", d.BaseURL, fileID)
	req, err := http.NewRequest("DELETE", requestURL, nil)
	if err != nil {
		return false, fmt.Errorf("failed to create request: %w", err)
	}
	for key, value := range headers {
		req.Header.Set(key, value)
	}

	resp, err := d.Client.Do(req)
	if err != nil {
		return false, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		// Attempt to read body for more detailed error info
		var errorResponse map[string]interface{}
		json.NewDecoder(resp.Body).Decode(&errorResponse)
		return false, fmt.Errorf("API error: status code %d, message: %v", resp.StatusCode, errorResponse["message"])
	}

	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return false, fmt.Errorf("failed to decode response: %w", err)
	}

	// Check for a success field in the response
	if success, ok := result["success"].(bool); ok && success {
		return true, nil
	}

	return false, fmt.Errorf("delete operation not confirmed by API response: %v", result)
}

func main() {
	deleter := NewGoFileDeleter(
		"MS_live_your_key_id_here",
		"your_api_secret_here",
	)

	// Replace with a valid file ID to delete
	fileID := "FsDxnT5Up8YjKyd"

	// --- Safety Confirmation ---
	fmt.Printf("Are you sure you want to delete the file with ID '%s'? (yes/no): ", fileID)
	reader := bufio.NewReader(os.Stdin)
	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(strings.ToLower(input))
	// -------------------------

	if input == "yes" || input == "y" {
		fmt.Println("Proceeding with deletion...")
		success, err := deleter.DeleteFile(fileID)
		if err != nil {
			fmt.Printf("❌ Error deleting file: %v\n", err)
			return
		}
		if success {
			fmt.Println("✅ File deleted successfully.")
		} else {
			fmt.Println("❓ Deletion failed for an unknown reason.")
		}
	} else {
		fmt.Println("Deletion cancelled by user.")
	}
}

Changelog

v1.1 - Large File Upload Support

August 18, 2025

🚀 New Features

  • Large File Upload Support: Files of any size can now be uploaded via the API
  • Automatic Chunking: Files larger than 10MB are automatically split into chunks
  • Rate Limiting Protection: Built-in delays and retries prevent server overload
  • Progress Tracking: Real-time upload progress feedback
  • Production-Ready Client: Complete Python API client with chunked upload support

🔧 Improvements

  • Enhanced Error Handling: Better error messages and recovery mechanisms
  • Signature Validation: Fixed multipart/form-data signature calculation
  • Memory Optimization: API middleware no longer loads large files into memory
  • Documentation: Comprehensive guides for large file uploads

🐛 Bug Fixes

  • Critical Bug Fix: Fixed undefined sha256() function in API middleware
  • Subscription Validation: Fixed API subscription checks to use proper helper functions
  • File Type Support: Added support for "file" and "pdf" types in list endpoint
  • Signature Mismatch: Fixed URL signature generation for query parameters

📚 Documentation

  • New Sections: Added "File Upload Limits & Chunking" documentation
  • Error Codes: Added new error codes for chunk size limits
  • Examples: Updated code examples with large file upload support
  • Limitations: Clear documentation of 90MB chunk size server limit

⚠️ Breaking Changes

  • Chunk Size Limit: Server now enforces maximum 90MB chunk size (was previously unlimited)
  • API Client: New production-ready client replaces basic examples

🔒 Security

  • Memory Protection: API middleware no longer loads large files into memory for signature verification
  • Rate Limiting: Enhanced protection against API abuse

v1.0 - Initial Release

January 2025

🎉 Initial Features

  • Basic File Operations: Upload, download, list, and delete files
  • HMAC Authentication: Secure API key authentication
  • File Management: Password protection, expiry dates, and metadata
  • Documentation: Complete API reference and examples