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/")
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");
}
}
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.")
}
}