Skip to content

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

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

feat: add file-sharing example with shareable download links#32
erhnysr wants to merge 3 commits into
shelby:mainfrom
erhnysr:feat/file-sharing-example

Conversation

@erhnysr
Copy link
Copy Markdown

@erhnysr erhnysr commented May 23, 2026

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
Adds a new isolated example Express app plus dependencies/lockfile updates; minimal risk to existing production code, with the main concern being typical file upload/streaming edge cases within the example.

Overview
Introduces a new apps/file-sharing example app that lets users upload a file to Shelby, generates a short shareable drop ID, and serves download links that stream the blob back from Shelby.

The server computes and returns a local SHA-256 hash for integrity, persists drop metadata (including expiry and download count) in drops.json with a simple write mutex, and includes new setup docs/config via README.md, .env.example, TypeScript config, and workspace lockfile dependency additions.

Reviewed by Cursor Bugbot for commit f660210. 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
Copy link
Copy Markdown

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

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 👍 / 👎.

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 👍 / 👎.

@erhnysr
Copy link
Copy Markdown
Author

erhnysr commented May 23, 2026

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
Copy link
Copy Markdown
Author

erhnysr commented May 23, 2026

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.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit f660210. Configure here.

saveDB(db);
});
return dbWriteLock;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Mutex promise chain permanently breaks on any error

Medium Severity

The updateDB function chains operations via .then() on dbWriteLock, but if any operation in the chain throws (e.g., corrupted drops.json causing JSON.parse to fail, or db[drop.id] being undefined on line 163), the resulting rejected promise becomes the new dbWriteLock. All subsequent .then() calls on a rejected promise skip execution, so every future updateDB call silently fails. This means after a single error, new uploads still push files to Shelby but their metadata is never persisted — download links become permanently broken. Adding a .catch() to reset the chain would prevent this cascading failure.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f660210. Configure here.

}

const { originalname, path: tmpPath, size } = req.file;
const safeName = originalname.replace(/[^a-zA-Z0-9._-]/g, "_");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicate filename sanitization logic not using shared function

Low Severity

Line 95 applies an inline regex (/[^a-zA-Z0-9._-]/g) to sanitize originalname, which is functionally identical to the sanitizeFilename function defined on line 71. Having two copies of the same sanitization logic means a future fix to one (e.g., tightening the allowed character set) could easily miss the other, risking inconsistent behavior.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f660210. Configure here.

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