コンテンツへスキップ
HexaTransfer
ブログへ戻る
技術詳解

ファイル転送サービスを自分で作る:開発チュートリアル

a ファイル転送 service from scratch. Backend API design, storage integration, and user interface development tutorial.

独自のファイル転送サービスを構築するには5つのピースが必要だ:ブラウザでファイルをスライスし任意で暗号化するフロントエンド、アップロードセッションを発行しストレージURLに署名するREST API、S3互換オブジェクトストレージ(Cloudflare R2、Backblaze B2、またはMinIO)、小さなメタデータデータベース(SQLiteまたはPostgres)、そしてTLS 1.3リバースプロキシ。Node 22、Fastify、Viteを使えば約600行のコードでWeTransferスタイルのサービスを出荷でき、適度なトラフィックで月5〜15ドルでホストできる。

まずストレージ層を選ぶ

ストレージの経済性がファイル転送サービスを左右する。AWS S3はGB当たりエグレス0.09ドルを課金するため、人気の2GBダウンロードが1,000回共有されると帯域幅だけで180ドルになる。Cloudflare R2はエグレスが無料で保存はGB当たり0.015ドル。Backblaze B2はエグレスがGB当たり0.01ドル、保存はGB当たり0.006ドル。無料転送サービスなら、R2が明らかな選択肢だ。MinIOは自分のハードウェア上でS3セマンティクスをセルフホストする場合に機能する。

クライアントがストレージに直接アップロード・ダウンロードするよう署名付きURLを使い、APIサーバーを通さないようにすること。これによりAPIがCPUバウンドになり帯域幅バウンドにならない。

APIサーフェスの設計

APIを小さく保つ。5つのエンドポイントで必要なものの95%がカバーできる:

  • POST /sessions — アップロードセッションを作成し、IDと署名付きマルチパートURLを返す
  • POST /sessions/:id/complete — マルチパートアップロードを完了し、共有URLを返す
  • GET /shares/:id — ファイルメタデータ(サイズ、有効期限、残りダウンロード数)を返す
  • GET /shares/:id/download — 署名付きダウンロードURLを返す
  • DELETE /shares/:id — オーナーに早期失効を許可(取消トークンが必要)

POST /sessionsを積極的にレート制限する—IPあたり時間20件で十分だ。全エンドポイントはJSONを返し、Cache-Control: no-storeを使い、HTMLページにContent-Security-Policyを設定する。

Fastifyでのバックエンド接続

Fastifyは試行なしにJSONスキーマ検証、適切なロギング、コアあたり30k req/sを提供する:

fastify.post('/sessions', {
  schema: {
    body: {
      type: 'object', required: ['size'],
      properties: {
        size: { type: 'integer', minimum: 1, maximum: 10737418240 },
        contentType: { type: 'string', maxLength: 120 }
      }
    }
  }
}, async (req) => {
  const id = nanoid(16);
  const key = `blobs/${id}`;
  const upload = await s3.createMultipartUpload({
    Bucket: 'transfers', Key: key, ContentType: req.body.contentType
  });
  const partCount = Math.ceil(req.body.size / (8 * 1024 * 1024));
  const urls = await Promise.all(
    Array.from({ length: partCount }, (_, i) =>
      getSignedUrl(s3, new UploadPartCommand({
        Bucket: 'transfers', Key: key, UploadId: upload.UploadId, PartNumber: i + 1
      }), { expiresIn: 3600 }))
  );
  db.prepare('INSERT INTO sessions VALUES (?, ?, ?, ?)')
    .run(id, upload.UploadId, key, Date.now());
  return { id, uploadId: upload.UploadId, partUrls: urls };
});

10GBの上限、8MBのパート、1時間のURL有効期限。トラフィックに応じて調整すること。

データベーススキーマの設計

better-sqlite3経由のSQLiteは問題なく数千万行を処理する。スキーマを最小限に保つ:

CREATE TABLE sessions (
  id TEXT PRIMARY KEY, upload_id TEXT, object_key TEXT, created_at INTEGER
);
CREATE TABLE shares (
  id TEXT PRIMARY KEY, object_key TEXT, size_bytes INTEGER,
  content_type TEXT, expires_at INTEGER, max_downloads INTEGER,
  download_count INTEGER DEFAULT 0, revocation_token TEXT
);

ユーザーテーブルなし、メールなし、IPログなし。これは意図的な選択—GDPR/APPI準拠をほぼ自明にする。毎晩のcronが期限切れの行とそのS3オブジェクトを削除する。

共有リンクとダウンロードの実装

クライアントサイド暗号化を追加した場合の共有URLはhttps://yourapp.com/f/abc123#k=<base64key>のようになる。フラグメントが鍵をサーバーログから遠ざける。ダウンロードページは/shares/abc123をフェッチし、署名付きS3 URLを取得し、R2から直接バイトをストリームし、ブラウザで復号し、StreamSaver.jsまたはFile System Access APIに結果を渡す。

Content-Disposition: attachment; filename*=UTF-8''encoded-nameを設定することで、ブラウザがインラインでレンダリングするのでなく保存ダイアログを表示する。これはPDF、HTMLファイル、SVGにとって重要で、これらはすべてブラウザコンテキストでスクリプトを実行できる。

悪用に対する強化

ファイル転送サービスは悪用者を引き付ける。3つのカテゴリが予想される:マルウェア配布、フィッシングペイロードのホスティング、著作権侵害のアップロード。影響の大きい順の軽減策:IPあたりのアップロードレート制限、最小サイズの要求(10KB未満はほぼ常にジャンク)、暗号文のSHA-256計算と既知の不正ハッシュのdenylistとの照合、摩擦のない悪用報告フォームの提供。

HexaTransferの構築アプローチはこの全体的な方法と一致している—FastifyのAPI、R2ストレージ、ブラウザのWeb Crypto、SQLiteメタデータ、アカウントなし。詳細は https://hexatransfer.com で。無料、アカウント不要、最大10GB。

エンドツーエンド暗号化で大容量ファイルを安全に送信

エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。

ファイルを送信