Skip to content

feat: add file-sharing example with shareable download links - #32

Open
erhnysr wants to merge 5 commits into
shelby:mainfrom
erhnysr:feat/file-sharing-example
Open

erhnysr wants to merge 5 commits into
shelby:mainfrom
erhnysr:feat/file-sharing-example

Conversation

@erhnysr

@erhnysr erhnysr commented May 23, 2026 •

Copy link
Copy Markdown

Summary

This PR adds a new example application demonstrating decentralized file sharing built on the Shelby SDK.

What it does

Upload any file → receive a unique shareable link → anyone can download it directly from Shelby. Every file is SHA-256 hashed locally before upload, providing verifiable integrity on Aptos blockchain.

Think of it as a decentralized alternative to WeTransfer.

How it works

  1. User uploads a file via the web UI (multipart/form-data)
  2. Server computes SHA-256 hash locally for integrity verification
  3. File is pushed to Shelby via client.upload()
  4. A unique drop ID is generated and metadata stored in drops.json
  5. Anyone with the share link hits /drop/:id/download to stream the file from Shelby via client.download()

API endpoints

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

Tech stack

  • TypeScript (strict mode)
  • Shelby Node SDK
  • Express.js
  • Aptos ts-sdk

Testing

Tested on Shelby testnet with successful uploads and downloads. Live demo: https://shelby-vibe-storage-production.up.railway.app

Checklist

  • TypeScript — zero errors
  • Follows repo structure (same as upload-blob, download-blob)
  • README with setup instructions
  • .env.example included
  • pnpm-lock.yaml updated

Note

Low Risk
Additive demo app only; no changes to core SDK or production services, though the example accepts arbitrary uploads and uses server-side Shelby credentials as documented for local/testnet use.

Overview
Adds a new apps/file-sharing example: a small Express + static HTML app that uploads files to Shelby testnet, stores drop metadata in local drops.json, and exposes shareable /d/:id links with /drop/:id/download streaming from the SDK.

Upload flow hashes files with SHA-256 before client.upload(), assigns short UUID-based drop IDs, and sets a 30-day blob TTL. The UI toggles between upload and drop views; the server serializes drops.json writes with a simple mutex and increments download counts on fetch.

Also ships .env.example, README, package.json (Express, multer, @shelby-protocol/sdk latest), tsconfig.json, and pnpm-lock.yaml entries for the new workspace package.

Reviewed by Cursor Bugbot for commit b049735. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread apps/file-sharing/src/server.ts
Comment thread apps/file-sharing/src/server.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba3a19abfe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/file-sharing/src/server.ts Outdated
Comment on lines +102 to +104
const db = loadDB();
db[id] = drop;
saveDB(db);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make drops.json updates atomic across concurrent requests

Avoid read-modify-write on drops.json without synchronization: /upload reads the file, mutates it, and writes it back after an await, so two overlapping uploads can each start from the same snapshot and the later write will silently discard the earlier drop record. The same pattern is used for download counters, so concurrent downloads can undercount. This causes real data loss/inaccurate metadata under normal parallel traffic.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f660210 with a write lock around drops.json updates. A follow-up in the latest commit keeps that lock usable after a failed update (see the Bugbot thread on updateDB).

const app = express();

app.use(express.json());
app.use(express.static(path.join(import.meta.dirname, "..", "public")));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serve an actual landing page for browser-based uploads

This middleware mounts ../public, but this commit does not add any apps/file-sharing/public files and there is no explicit GET / handler, so visiting http://localhost:3000 returns 404 and the documented browser upload flow is unavailable. Either include the frontend assets in this app or provide a route that returns an upload page.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply that this was resolved was wrong: f660210 added the /d/:id route but no public directory was ever committed, so / and every share link returned 404. The latest commit adds public/index.html, an upload form that shows the share link and SHA-256, and the download page for /d/:id that reads /drop/:id. Checked locally: / and /d/:id now return the page, and the drop page renders a seeded drop with its hash and download link.

@erhnysr

erhnysr commented May 23, 2026

Copy link
Copy Markdown
Author

Addressed the two issues flagged by Cursor Bugbot:

  1. Content-Disposition header injection — added sanitizeFilename() that strips non-safe characters before setting the header
  2. Download error after headers sent — moved header writes before the stream starts and added res.headersSent guard in the catch block

Both fixes are in commit ea2c1b6.

@erhnysr

erhnysr commented May 23, 2026

Copy link
Copy Markdown
Author

Follow-up fixes based on Codex suggestions:

  1. Race condition on drops.json — added a simple async mutex (dbWriteLock) so concurrent reads/writes are serialized via promise chaining
  2. Landing page for /d/:id routes — added a route that serves index.html so shareable drop links work correctly in the browser

All 4 issues are now resolved. PR is ready for review.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread apps/file-sharing/src/server.ts
Comment thread apps/file-sharing/src/server.ts Outdated
…ock usable after an error

The app served ../public and sent public/index.html for /d/:id links, but no
public directory was committed, so / and every share link returned 404. Add
public/index.html: an upload form that shows the share link and SHA-256, and
the download page for /d/:id that reads /drop/:id.

updateDB chained each update onto the previous promise, so one failed update
(a corrupt drops.json, for example) left the lock rejected and every later
update was skipped without running. The caller still gets its own error, but
the lock now always settles. The download counter also no longer throws when
the drop is missing from the file.

Use sanitizeFilename for the blob name instead of a second copy of the same
regex, apply the formatting biome check asks for, and list
SHELBY_ACCOUNT_ADDRESS in the README, which the server requires.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 38b4ac7. Configure here.

Comment thread apps/file-sharing/public/index.html
… slash

Express serves index.html for /d/:id/ as well as /d/:id, but the page only
matched the unslashed path, so a link with a trailing slash showed the upload
form instead of the file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant