Repository navigation
feat: add file-sharing example with shareable download links #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
erhnysr
wants to merge
5
commits into
shelby:main
Choose a base branch
from
erhnysr:feat/file-sharing-example
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ba3a19a
feat: add file-sharing example with shareable download links
erhnysr ea2c1b6
fix: sanitize Content-Disposition filename and guard against post-hea…
erhnysr f660210
fix: add async write mutex for drops.json and serve landing page for …
erhnysr 38b4ac7
fix(file-sharing): add the missing frontend and keep the drops.json l…
erhnysr b049735
fix(file-sharing): open the drop page for share links with a trailing…
erhnysr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.