Platform Integration Guides
Fula is S3-compatible, so you can use any S3 SDK. Here's how to integrate with popular platforms.
🌐 Next.js / React
Use the AWS SDK for JavaScript v3 in your Next.js application for both client and server components.
Installation
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Use Cases
- Server Actions - Upload from API routes
- Presigned URLs - Direct browser uploads
- Server Components - Fetch files server-side
- Edge Runtime - Works with Edge functions
Environment Variables
Add to .env.local:
FULA_ENDPOINT=http://localhost:9000
FULA_ACCESS_KEY=your-access-key
FULA_SECRET_KEY=your-secret-key
FULA_BUCKET=my-bucket
import { S3Client } from '@aws-sdk/client-s3';
export const fulaClient = new S3Client({
endpoint: process.env.FULA_ENDPOINT,
region: 'us-east-1',
credentials: {
accessKeyId: process.env.FULA_ACCESS_KEY!,
secretAccessKey: process.env.FULA_SECRET_KEY!,
},
forcePathStyle: true,
});
import { NextRequest, NextResponse } from 'next/server';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { fulaClient } from '@/lib/fula';
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('file') as File;
const buffer = Buffer.from(await file.arrayBuffer());
await fulaClient.send(new PutObjectCommand({
Bucket: process.env.FULA_BUCKET,
Key: `uploads/${file.name}`,
Body: buffer,
ContentType: file.type,
}));
return NextResponse.json({
success: true,
key: `uploads/${file.name}`
});
}
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
// Generate upload URL (valid 1 hour)
export async function getUploadUrl(key: string) {
const command = new PutObjectCommand({
Bucket: process.env.FULA_BUCKET,
Key: key,
});
return getSignedUrl(fulaClient, command, { expiresIn: 3600 });
}
// Generate download URL
export async function getDownloadUrl(key: string) {
const command = new GetObjectCommand({
Bucket: process.env.FULA_BUCKET,
Key: key,
});
return getSignedUrl(fulaClient, command, { expiresIn: 3600 });
}
// Client-side: Upload directly to Fula
const uploadUrl = await getUploadUrl('my-file.pdf');
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
📱 React Native (iOS & Android)
Use react-native-blob-util for file handling with AWS SDK or direct HTTP calls.
Installation
npm install aws-sdk react-native-blob-util
Approaches
- Presigned URLs - Recommended for mobile
- Backend Proxy - Upload via your server
- Direct SDK - AWS SDK v2 works in RN
Note
AWS SDK v3 has issues with React Native. Use v2 or presigned URLs.
import AWS from 'aws-sdk';
const fulaS3 = new AWS.S3({
endpoint: 'https://your-fula-gateway.com',
accessKeyId: 'YOUR_ACCESS_KEY',
secretAccessKey: 'YOUR_SECRET_KEY',
s3ForcePathStyle: true,
signatureVersion: 'v4',
});
export default fulaS3;
import ReactNativeBlobUtil from 'react-native-blob-util';
// Get presigned URL from your backend
const response = await fetch('https://your-api.com/upload-url', {
method: 'POST',
body: JSON.stringify({ filename: 'photo.jpg' }),
});
const { uploadUrl } = await response.json();
// Upload file directly to Fula
const filePath = '/path/to/photo.jpg';
await ReactNativeBlobUtil.fetch('PUT', uploadUrl, {
'Content-Type': 'image/jpeg',
}, ReactNativeBlobUtil.wrap(filePath));
console.log('Upload complete!');
import { launchCamera } from 'react-native-image-picker';
import fulaS3 from './fulaClient';
async function uploadPhoto() {
const result = await launchCamera({ mediaType: 'photo' });
if (result.assets?.[0]) {
const asset = result.assets[0];
const response = await fetch(asset.uri!);
const blob = await response.blob();
await fulaS3.upload({
Bucket: 'photos',
Key: `camera/${Date.now()}.jpg`,
Body: blob,
ContentType: 'image/jpeg',
}).promise();
console.log('Photo uploaded!');
}
}
// Download file
async function downloadFile(key: string) {
const url = fulaS3.getSignedUrl('getObject', {
Bucket: 'photos',
Key: key,
Expires: 3600,
});
const res = await ReactNativeBlobUtil.config({
fileCache: true,
appendExt: 'jpg',
}).fetch('GET', url);
return res.path();
}
🪟 .NET / C# (Windows, MAUI, Blazor)
Use the official AWS SDK for .NET. Works with Windows apps, ASP.NET, MAUI, and Blazor.
Installation
dotnet add package AWSSDK.S3
Platforms
- ASP.NET Core - Web APIs and MVC
- Blazor - Server and WASM
- MAUI - Cross-platform mobile/desktop
- WPF/WinForms - Windows desktop
- Console - CLI tools
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Runtime;
public class FulaClient
{
private readonly IAmazonS3 _s3Client;
private readonly string _bucketName;
public FulaClient(string endpoint, string accessKey,
string secretKey, string bucket)
{
var config = new AmazonS3Config
{
ServiceURL = endpoint,
ForcePathStyle = true,
SignatureVersion = "4"
};
var credentials = new BasicAWSCredentials(accessKey, secretKey);
_s3Client = new AmazonS3Client(credentials, config);
_bucketName = bucket;
}
public async Task UploadFileAsync(string key, Stream stream,
string contentType)
{
var request = new PutObjectRequest
{
BucketName = _bucketName,
Key = key,
InputStream = stream,
ContentType = contentType
};
await _s3Client.PutObjectAsync(request);
}
public async Task<Stream> DownloadFileAsync(string key)
{
var response = await _s3Client.GetObjectAsync(_bucketName, key);
return response.ResponseStream;
}
public async Task<List<S3Object>> ListFilesAsync(string prefix = "")
{
var request = new ListObjectsV2Request
{
BucketName = _bucketName,
Prefix = prefix
};
var response = await _s3Client.ListObjectsV2Async(request);
return response.S3Objects;
}
public string GetPresignedUrl(string key, TimeSpan expiry)
{
var request = new GetPreSignedUrlRequest
{
BucketName = _bucketName,
Key = key,
Expires = DateTime.UtcNow.Add(expiry)
};
return _s3Client.GetPreSignedURL(request);
}
}
// Program.cs - Dependency Injection
builder.Services.AddSingleton<FulaClient>(sp =>
new FulaClient(
endpoint: "http://localhost:9000",
accessKey: builder.Configuration["Fula:AccessKey"]!,
secretKey: builder.Configuration["Fula:SecretKey"]!,
bucket: "my-bucket"
));
// Controller
[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly FulaClient _fula;
public FilesController(FulaClient fula) => _fula = fula;
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
var key = $"uploads/{Guid.NewGuid()}/{file.FileName}";
await _fula.UploadFileAsync(key, file.OpenReadStream(),
file.ContentType);
return Ok(new { key });
}
[HttpGet("download/{*key}")]
public async Task<IActionResult> Download(string key)
{
var stream = await _fula.DownloadFileAsync(key);
return File(stream, "application/octet-stream");
}
}
// Upload photo from device
public async Task UploadPhotoAsync()
{
var result = await MediaPicker.CapturePhotoAsync();
if (result == null) return;
using var stream = await result.OpenReadAsync();
var key = $"photos/{DateTime.Now:yyyyMMdd}/{result.FileName}";
await _fula.UploadFileAsync(key, stream, "image/jpeg");
await DisplayAlert("Success", "Photo uploaded!", "OK");
}
🐦 Flutter / Dart
Use the minio package for S3-compatible storage in Flutter apps.
Installation
flutter pub add minio
Platforms
Works on iOS, Android, Web, Windows, macOS, and Linux.
import 'package:minio/minio.dart';
import 'dart:io';
class FulaClient {
late final Minio _minio;
final String bucket;
FulaClient({
required String endpoint,
required String accessKey,
required String secretKey,
required this.bucket,
}) {
_minio = Minio(
endPoint: endpoint,
accessKey: accessKey,
secretKey: secretKey,
useSSL: endpoint.startsWith('https'),
);
}
Future<void> uploadFile(String key, File file) async {
await _minio.fPutObject(bucket, key, file.path);
}
Future<void> uploadBytes(String key, List<int> data,
String contentType) async {
await _minio.putObject(
bucket,
key,
Stream.value(data),
size: data.length,
metadata: {'Content-Type': contentType},
);
}
Future<List<int>> downloadFile(String key) async {
final stream = await _minio.getObject(bucket, key);
final chunks = <int>[];
await for (final chunk in stream) {
chunks.addAll(chunk);
}
return chunks;
}
Future<List<Object>> listFiles(String prefix) async {
final objects = <Object>[];
await for (final obj in _minio.listObjects(bucket, prefix: prefix)) {
objects.addAll(obj.objects);
}
return objects;
}
Future<String> getPresignedUrl(String key) async {
return await _minio.presignedGetObject(bucket, key);
}
}
import 'package:image_picker/image_picker.dart';
final fula = FulaClient(
endpoint: 'your-fula-gateway.com',
accessKey: 'YOUR_ACCESS_KEY',
secretKey: 'YOUR_SECRET_KEY',
bucket: 'my-bucket',
);
// Upload image from gallery
Future<void> uploadImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
final file = File(image.path);
final key = 'images/${DateTime.now().millisecondsSinceEpoch}.jpg';
await fula.uploadFile(key, file);
print('Uploaded: $key');
}
}
// Display image from Fula
Widget buildImage(String key) {
return FutureBuilder<String>(
future: fula.getPresignedUrl(key),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.network(snapshot.data!);
}
return CircularProgressIndicator();
},
);
}
🍎 Swift / iOS
Use the AWS SDK for Swift or Soto (community SDK) for iOS/macOS apps.
Installation (SPM)
https://github.com/soto-project/soto.git
Recommended
Soto is lighter and more Swift-native than the official AWS SDK.
import SotoS3
import NIO
class FulaClient {
private let s3: S3
private let bucket: String
init(endpoint: String, accessKey: String,
secretKey: String, bucket: String) {
let client = AWSClient(
credentialProvider: .static(
accessKeyId: accessKey,
secretAccessKey: secretKey
),
httpClientProvider: .createNew
)
self.s3 = S3(
client: client,
endpoint: endpoint,
timeout: .seconds(30)
)
self.bucket = bucket
}
func upload(key: String, data: Data,
contentType: String) async throws {
let request = S3.PutObjectRequest(
body: .data(data),
bucket: bucket,
contentType: contentType,
key: key
)
_ = try await s3.putObject(request)
}
func download(key: String) async throws -> Data {
let request = S3.GetObjectRequest(bucket: bucket, key: key)
let response = try await s3.getObject(request)
guard let body = response.body?.asData() else {
throw NSError(domain: "FulaError", code: 1)
}
return body
}
func listObjects(prefix: String) async throws -> [S3.Object] {
let request = S3.ListObjectsV2Request(
bucket: bucket,
prefix: prefix
)
let response = try await s3.listObjectsV2(request)
return response.contents ?? []
}
}
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var selectedImage: PhotosPickerItem?
let fula = FulaClient(
endpoint: "https://your-fula-gateway.com",
accessKey: "YOUR_KEY",
secretKey: "YOUR_SECRET",
bucket: "photos"
)
var body: some View {
PhotosPicker(selection: $selectedImage, matching: .images) {
Text("Upload Photo")
}
.onChange(of: selectedImage) { item in
Task {
if let data = try? await item?.loadTransferable(
type: Data.self
) {
let key = "photos/\(UUID().uuidString).jpg"
try? await fula.upload(
key: key,
data: data,
contentType: "image/jpeg"
)
}
}
}
}
}
🤖 Kotlin / Android
Use the AWS SDK for Kotlin or MinIO Java SDK for Android apps.
Gradle Dependency
implementation("io.minio:minio:8.5.7")
Note
MinIO SDK is lighter than AWS SDK for mobile use.
import io.minio.MinioClient
import io.minio.PutObjectArgs
import io.minio.GetObjectArgs
import io.minio.ListObjectsArgs
import java.io.InputStream
class FulaClient(
endpoint: String,
accessKey: String,
secretKey: String,
private val bucket: String
) {
private val client = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build()
suspend fun upload(
key: String,
stream: InputStream,
size: Long,
contentType: String
) {
withContext(Dispatchers.IO) {
client.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.`object`(key)
.stream(stream, size, -1)
.contentType(contentType)
.build()
)
}
}
suspend fun download(key: String): InputStream {
return withContext(Dispatchers.IO) {
client.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.`object`(key)
.build()
)
}
}
suspend fun listObjects(prefix: String): List<String> {
return withContext(Dispatchers.IO) {
client.listObjects(
ListObjectsArgs.builder()
.bucket(bucket)
.prefix(prefix)
.build()
).map { it.get().objectName() }
}
}
}
@Composable
fun UploadScreen() {
val context = LocalContext.current
val fula = remember {
FulaClient(
endpoint = "https://your-fula-gateway.com",
accessKey = "YOUR_KEY",
secretKey = "YOUR_SECRET",
bucket = "uploads"
)
}
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
lifecycleScope.launch {
val stream = context.contentResolver.openInputStream(uri)
val key = "uploads/${System.currentTimeMillis()}.jpg"
stream?.use { input ->
fula.upload(
key = key,
stream = input,
size = input.available().toLong(),
contentType = "image/jpeg"
)
}
}
}
}
Button(onClick = { launcher.launch("image/*") }) {
Text("Upload Image")
}
}
📁 File Manager Pattern
Build file browsers that work with encrypted storage without downloading all file content.
The Challenge
With encrypted storage, you might think you need to download and decrypt every file just to show a file list. That's expensive!
The Solution
Fula encrypts metadata separately, allowing you to:
- List files - Get filenames without downloading content
- Show details - Size, type, timestamps from metadata only
- Lazy load - Download content only when user opens file
Bandwidth Comparison
| Operation | Without Optimization | With Metadata API |
|---|---|---|
| List 100 files (1GB total) | Download 1GB | ~100KB (headers) |
| Show file info | Full download | HEAD request |
use fula_client::{EncryptedClient, EncryptionConfig, Config};
async fn build_file_browser(client: &EncryptedClient, bucket: &str) {
// Step 1: List all files (metadata only - fast!)
let files = client.list_objects_decrypted(bucket, None).await?;
// Step 2: Display in UI
for file in &files {
println!("📄 {} - {} - {:?}",
file.original_key, // Real filename (decrypted)
file.size_human(), // "1.5 MB"
file.content_type // "application/pdf"
);
}
// Step 3: User clicks a file - NOW download content
if let Some(selected) = user_selection {
let content = client.get_object_decrypted_by_storage_key(
bucket,
&selected.storage_key
).await?;
open_file(content);
}
}
// For TypeScript/JavaScript apps, use the Rust SDK via WASM
// or implement the metadata pattern with HEAD requests
interface FileMetadata {
storageKey: string;
originalKey: string;
size: number;
contentType?: string;
createdAt?: number;
}
// Efficient: Only fetch metadata headers
async function listFilesMetadataOnly(bucket: string): Promise {
// 1. List object keys (S3 ListObjectsV2)
const listResult = await s3.send(new ListObjectsV2Command({ Bucket: bucket }));
// 2. HEAD each object to get metadata without content
const files = await Promise.all(
(listResult.Contents ?? []).map(async (obj) => {
const head = await s3.send(new HeadObjectCommand({
Bucket: bucket,
Key: obj.Key!
}));
// Decrypt private metadata from x-amz-meta-encryption header
const encMeta = head.Metadata?.['encryption'];
const privateMeta = encMeta ? decryptMetadata(encMeta) : null;
return {
storageKey: obj.Key!,
originalKey: privateMeta?.originalKey ?? obj.Key!,
size: privateMeta?.actualSize ?? obj.Size!,
contentType: privateMeta?.contentType,
};
})
);
return files;
}
// React component
function FileBrowser({ bucket }: { bucket: string }) {
const [files, setFiles] = useState([]);
useEffect(() => {
listFilesMetadataOnly(bucket).then(setFiles);
}, [bucket]);
return (
<div className="file-grid">
{files.map(file => (
<FileCard
key={file.storageKey}
name={file.originalKey}
size={formatSize(file.size)}
type={file.contentType}
onClick={() => downloadFile(file.storageKey)}
/>
))}
</div>
);
}
class FileMetadata {
final String storageKey;
final String originalKey;
final int size;
final String? contentType;
FileMetadata({
required this.storageKey,
required this.originalKey,
required this.size,
this.contentType,
});
}
class FulaFileBrowser extends StatefulWidget {
@override
_FulaFileBrowserState createState() => _FulaFileBrowserState();
}
class _FulaFileBrowserState extends State {
List files = [];
bool loading = true;
@override
void initState() {
super.initState();
loadFileMetadata();
}
Future loadFileMetadata() async {
// Only fetch metadata, not file content
final objects = await fulaClient.listObjects(bucket);
final metadata = await Future.wait(
objects.map((obj) => fulaClient.headObject(bucket, obj.key))
);
setState(() {
files = metadata.map((m) => FileMetadata(
storageKey: m.key,
originalKey: decryptMetadata(m)?.originalKey ?? m.key,
size: decryptMetadata(m)?.actualSize ?? m.size,
contentType: decryptMetadata(m)?.contentType,
)).toList();
loading = false;
});
}
@override
Widget build(BuildContext context) {
if (loading) return CircularProgressIndicator();
return ListView.builder(
itemCount: files.length,
itemBuilder: (ctx, i) => ListTile(
leading: Icon(getFileIcon(files[i].contentType)),
title: Text(files[i].originalKey.split('/').last),
subtitle: Text(formatSize(files[i].size)),
onTap: () => downloadAndOpen(files[i].storageKey),
),
);
}
}