Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/file-sharing/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SHELBY_ACCOUNT_PRIVATE_KEY=your_private_key_here
SHELBY_API_KEY=your_api_key_here
SHELBY_ACCOUNT_ADDRESS=your_account_address_here
PORT=3000
67 changes: 67 additions & 0 deletions apps/file-sharing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Shelby File Sharing Example

A full-stack example demonstrating decentralized file sharing built on the Shelby SDK. Upload any file, receive a unique shareable link, and let anyone download it — every file is SHA-256 hashed and stored on Shelby decentralized hot storage with Aptos blockchain provenance.

Think of it as a decentralized alternative to WeTransfer.

## Prerequisites

- Node.js >= 22
- pnpm
- A Shelby account with testnet tokens
- Shelby API key

## Installation

Navigate to the file-sharing directory and install dependencies:

cd apps/file-sharing
pnpm install

## Environment Variables

Copy the example file and fill in your credentials:

cp .env.example .env

Required variables:

SHELBY_ACCOUNT_PRIVATE_KEY=your_private_key_here
SHELBY_API_KEY=your_api_key_here
SHELBY_ACCOUNT_ADDRESS=your_account_address_here
PORT=3000

## Usage

pnpm dev

Open http://localhost:3000 in your browser.

## How It Works

1. Upload — User selects a file; the server pushes it to Shelby via client.upload()
2. Hash — A SHA-256 digest is computed locally before upload for integrity verification
3. Link — A unique drop ID is generated and stored in drops.json
4. Share — Anyone with the link can hit /drop/:id/download to stream the file from Shelby via client.download()
5. Verify — The SHA-256 hash is displayed on the download page so recipients can verify file integrity

## API

| Method | Path | Description |
|--------|------|-------------|
| POST | /upload | Upload a file multipart/form-data |
| GET | /drop/:id | Get drop metadata |
| GET | /drop/:id/download | Download file from Shelby |
| GET | /drops | List all drops |

## Project Structure

apps/file-sharing/
src/
server.ts # Express server + Shelby SDK integration
public/
index.html # Upload page, and the download page served for /d/:id links
.env.example
package.json
README.md
tsconfig.json
38 changes: 38 additions & 0 deletions apps/file-sharing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@shelby-protocol/file-sharing-example",
"version": "1.0.0",
"description": "An example app demonstrating decentralized file sharing with shareable download links using the Shelby SDK",
"type": "module",
"scripts": {
"dev": "tsx --env-file=.env src/server.ts",
"lint": "biome check .",
"fmt": "biome check . --write"
},
"keywords": [
"shelby",
"sdk",
"blob",
"storage",
"file-sharing"
],
"author": "erhnysr",
"license": "MIT",
"dependencies": {
"@aptos-labs/ts-sdk": "^5.1.1",
"@shelby-protocol/sdk": "latest",
"express": "^4.21.2",
"multer": "^2.0.1",
"uuid": "^11.1.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@types/express": "^5.0.3",
"@types/multer": "^1.4.12",
"@types/uuid": "^10.0.0",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
},
"engines": {
"node": ">=22"
}
}
120 changes: 120 additions & 0 deletions apps/file-sharing/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Shelby File Sharing</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 560px;
margin: 48px auto;
padding: 0 16px;
color: #1a1a1a;
}
h1 {
font-size: 1.4rem;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.hash {
font-family: ui-monospace, monospace;
font-size: 0.8rem;
word-break: break-all;
}
.error {
color: #b00020;
}
[hidden] {
display: none;
}
</style>
</head>
<body>
<h1>Shelby File Sharing</h1>

<section id="upload-view" hidden>
<form id="upload-form" class="card">
<input type="file" name="file" required />
<button type="submit">Upload</button>
</form>
<div id="upload-result" class="card" hidden>
<p>Share this link:</p>
<p><a id="share-link"></a></p>
<p>SHA-256: <span id="upload-hash" class="hash"></span></p>
<p>Expires: <span id="upload-expires"></span></p>
</div>
</section>

<section id="drop-view" class="card" hidden>
<p><strong id="drop-name"></strong> (<span id="drop-size"></span>)</p>
<p>SHA-256: <span id="drop-hash" class="hash"></span></p>
<p>Expires: <span id="drop-expires"></span></p>
<p><a id="drop-download">Download</a></p>
</section>

<p id="error" class="error" hidden></p>

<script>
const $ = (id) => document.getElementById(id);

function showError(message) {
$("error").textContent = message;
$("error").hidden = false;
}

function formatSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

async function showDrop(id) {
$("drop-view").hidden = false;
const res = await fetch(`/drop/${encodeURIComponent(id)}`);
if (!res.ok) {
$("drop-view").hidden = true;
showError("This drop was not found.");
return;
}
const drop = await res.json();
$("drop-name").textContent = drop.fileName;
$("drop-size").textContent = formatSize(drop.fileSize);
$("drop-hash").textContent = drop.sha256;
$("drop-expires").textContent = new Date(drop.expiresAt).toLocaleString();
$("drop-download").href = `/drop/${encodeURIComponent(id)}/download`;
}

function showUpload() {
$("upload-view").hidden = false;
$("upload-form").addEventListener("submit", async (event) => {
event.preventDefault();
$("error").hidden = true;
const res = await fetch("/upload", {
method: "POST",
body: new FormData(event.target),
});
const body = await res.json();
if (!res.ok) {
showError(body.error ?? "Upload failed.");
return;
}
const link = `${location.origin}/d/${body.id}`;
$("share-link").href = link;
$("share-link").textContent = link;
$("upload-hash").textContent = body.sha256;
$("upload-expires").textContent = new Date(body.expiresAt).toLocaleString();
$("upload-result").hidden = false;
});
}

const match = location.pathname.match(/^\/d\/([^/]+)\/?$/);
if (match) showDrop(decodeURIComponent(match[1]));
else showUpload();
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
</script>
</body>
</html>
Loading