@@ -416,62 +416,165 @@ <h3>📦 Content-Defined Chunking</h3>
416416 <!-- Getting Started Section -->
417417 < section id ="getting-started " class ="intro-section ">
418418 < h2 > Getting Started</ h2 >
419- < div class ="getting-started-grid ">
419+
420+ <!-- Row 1: Setup -->
421+ < h3 class ="section-subtitle "> 1. Setup Your Environment</ h3 >
422+ < div class ="getting-started-grid setup-row ">
420423 < div class ="gs-card ">
421424 < h3 > 🐳 Docker Compose</ h3 >
422- < p > Quickest way to run a local Fula gateway :</ p >
425+ < p > Quickest way to run locally :</ p >
423426 < div class ="code-block ">
424427 < pre > git clone https://github.com/functionland/fula-api
425428cd fula-api
426- docker-compose up -d
427-
428- # Gateway available at http://localhost:9000</ pre >
429+ docker-compose up -d</ pre >
429430 </ div >
430431 </ div >
431432 < div class ="gs-card ">
432433 < h3 > 🔧 From Source</ h3 >
433434 < p > Build and run with Cargo:</ p >
434435 < div class ="code-block ">
435- < pre > git clone https://github.com/functionland/fula-api
436- cd fula-api
437- cargo build --release
436+ < pre > cargo build --release
438437./target/release/fula-cli serve</ pre >
439438 </ div >
440439 </ div >
441440 < div class ="gs-card ">
442441 < h3 > ⚙️ Configuration</ h3 >
443- < p > Set environment variables:</ p >
442+ < p > Environment variables:</ p >
444443 < div class ="code-block ">
445- < pre > # Gateway
446- FULA_HOST=0.0.0.0
444+ < pre > FULA_HOST=0.0.0.0
447445FULA_PORT=9000
448-
449- # IPFS
450446IPFS_API_URL=http://localhost:5001
451- CLUSTER_API_URL=http://localhost:9094
452-
453- # Auth
454447JWT_SECRET=your-secret-key</ pre >
455448 </ div >
456449 </ div >
457- < div class ="gs-card ">
458- < h3 > 🧪 Test with AWS CLI</ h3 >
459- < p > Verify your setup:</ p >
460- < div class ="code-block ">
461- < pre > # Configure endpoint
462- alias fula='aws --endpoint-url http://localhost:9000'
450+ </ div >
463451
464- # Create bucket
465- fula s3 mb s3://test-bucket
452+ <!-- Row 2: Quick Start Code Example -->
453+ < h3 class ="section-subtitle "> 2. Upload & Manage Photos (JavaScript)</ h3 >
454+ < div class ="quickstart-code ">
455+ < div class ="code-header ">
456+ < span class ="code-title "> 📸 Complete Example: Encrypt, Upload, and List Photos</ span >
457+ < span class ="code-lang "> JavaScript / TypeScript</ span >
458+ </ div >
459+ < div class ="code-block large ">
460+ < pre > < span class ="code-comment "> // 1. Install dependencies</ span >
461+ < span class ="code-comment "> // npm install @aws-sdk/client-s3 crypto-js</ span >
466462
467- # Upload file
468- fula s3 cp ./file.txt s3://test-bucket/
463+ import { S3Client, PutObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
469464
470- # List files
471- fula s3 ls s3://test-bucket/</ pre >
472- </ div >
465+ < span class ="code-comment "> // 2. Configure the Fula client</ span >
466+ const fula = new S3Client({
467+ endpoint: 'http://localhost:9000',
468+ region: 'us-east-1',
469+ credentials: {
470+ accessKeyId: 'YOUR_ACCESS_KEY',
471+ secretAccessKey: 'YOUR_SECRET_KEY',
472+ },
473+ forcePathStyle: true,
474+ });
475+
476+ const BUCKET = 'my-photos';
477+
478+ < span class ="code-comment "> // 3. Helper: Encrypt data before upload (client-side encryption)</ span >
479+ async function encryptData(data: ArrayBuffer, password: string): Promise<ArrayBuffer> {
480+ const encoder = new TextEncoder();
481+ const keyMaterial = await crypto.subtle.importKey(
482+ 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']
483+ );
484+ const key = await crypto.subtle.deriveKey(
485+ { name: 'PBKDF2', salt: encoder.encode('fula-salt'), iterations: 100000, hash: 'SHA-256' },
486+ keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']
487+ );
488+ const iv = crypto.getRandomValues(new Uint8Array(12));
489+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
490+ < span class ="code-comment "> // Prepend IV to encrypted data</ span >
491+ const result = new Uint8Array(iv.length + encrypted.byteLength);
492+ result.set(iv);
493+ result.set(new Uint8Array(encrypted), iv.length);
494+ return result.buffer;
495+ }
496+
497+ < span class ="code-comment "> // 4. Upload an encrypted photo</ span >
498+ async function uploadPhoto(filename: string, photoData: ArrayBuffer, password: string) {
499+ console.log(`📤 Encrypting and uploading: ${filename}`);
500+
501+ < span class ="code-comment "> // Encrypt locally BEFORE sending to gateway</ span >
502+ const encryptedData = await encryptData(photoData, password);
503+
504+ await fula.send(new PutObjectCommand({
505+ Bucket: BUCKET,
506+ Key: `photos/${filename}`,
507+ Body: new Uint8Array(encryptedData),
508+ ContentType: 'application/octet-stream', < span class ="code-comment "> // Encrypted = opaque bytes</ span >
509+ Metadata: {
510+ 'x-amz-meta-encrypted': 'true',
511+ 'x-amz-meta-original-type': 'image/jpeg',
512+ },
513+ }));
514+
515+ console.log(`✅ Uploaded: photos/${filename}`);
516+ }
517+
518+ < span class ="code-comment "> // 5. List all photos in the folder</ span >
519+ async function listPhotos() {
520+ console.log('📂 Listing photos folder...');
521+
522+ const response = await fula.send(new ListObjectsV2Command({
523+ Bucket: BUCKET,
524+ Prefix: 'photos/',
525+ }));
526+
527+ const files = response.Contents || [];
528+ console.log(`Found ${files.length} photo(s):`);
529+ files.forEach(file => {
530+ console.log(` 📷 ${file.Key} (${file.Size} bytes)`);
531+ });
532+
533+ return files;
534+ }
535+
536+ < span class ="code-comment "> // 6. Run the complete workflow</ span >
537+ async function main() {
538+ const password = 'my-secret-encryption-key';
539+
540+ < span class ="code-comment "> // Simulate photo data (in real app, read from file/camera)</ span >
541+ const photo1 = new TextEncoder().encode('fake-jpeg-data-for-sunset.jpg');
542+ const photo2 = new TextEncoder().encode('fake-jpeg-data-for-beach.jpg');
543+
544+ < span class ="code-comment "> // Upload first photo (encrypted)</ span >
545+ await uploadPhoto('sunset.jpg', photo1.buffer, password);
546+
547+ < span class ="code-comment "> // List folder - should show 1 photo</ span >
548+ await listPhotos();
549+ < span class ="code-comment "> // Output: Found 1 photo(s): 📷 photos/sunset.jpg</ span >
550+
551+ < span class ="code-comment "> // Upload second photo (encrypted)</ span >
552+ await uploadPhoto('beach.jpg', photo2.buffer, password);
553+
554+ < span class ="code-comment "> // List folder again - should show 2 photos</ span >
555+ await listPhotos();
556+ < span class ="code-comment "> // Output: Found 2 photo(s): 📷 photos/sunset.jpg, 📷 photos/beach.jpg</ span >
557+ }
558+
559+ main().catch(console.error);</ pre >
560+ </ div >
561+ < div class ="code-output ">
562+ < div class ="output-header "> Expected Output</ div >
563+ < pre > 📤 Encrypting and uploading: sunset.jpg
564+ ✅ Uploaded: photos/sunset.jpg
565+ 📂 Listing photos folder...
566+ Found 1 photo(s):
567+ 📷 photos/sunset.jpg (156 bytes)
568+
569+ 📤 Encrypting and uploading: beach.jpg
570+ ✅ Uploaded: photos/beach.jpg
571+ 📂 Listing photos folder...
572+ Found 2 photo(s):
573+ 📷 photos/sunset.jpg (156 bytes)
574+ 📷 photos/beach.jpg (152 bytes)</ pre >
473575 </ div >
474576 </ div >
577+
475578 < div class ="next-steps ">
476579 < h3 > Next Steps</ h3 >
477580 < div class ="next-links ">
0 commit comments