> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare R2 file upload and management system

> Serverless file upload and management system on Cloudflare Workers and R2 object storage with no size limit, suited to large model files, deployed via wrangler.

export const UploadDemo = ({lang = "zh"}) => {
  const t = lang === "en" ? {
    drop: "Drag files here, or",
    btn: "Simulate upload",
    uploading: "Uploading",
    done: "Uploaded",
    empty: "No files yet",
    hint: "No size limit. Backed by Cloudflare R2."
  } : {
    drop: "把檔案拖到這裡，或",
    btn: "模擬上傳",
    uploading: "上傳中",
    done: "已上傳",
    empty: "尚無檔案",
    hint: "無檔案大小限制，後端為 Cloudflare R2。"
  };
  const SAMPLES = [{
    name: "model.safetensors",
    size: "1.24 GB",
    kind: "file"
  }, {
    name: "checkpoint-epoch12.ckpt",
    size: "860 MB",
    kind: "file"
  }, {
    name: "sam2.1_hiera_large.pt",
    size: "2.4 GB",
    kind: "file"
  }, {
    name: "lora_adapter.safetensors",
    size: "148 MB",
    kind: "file"
  }, {
    name: "dataset_v3.zip",
    size: "5.1 GB",
    kind: "archive"
  }];
  const fileIcon = kind => kind === "archive" ? <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" /><path d="M3.3 7 12 12l8.7-5" /><path d="M12 22V12" /></svg> : <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /></svg>;
  const [files, setFiles] = useState([]);
  const [seq, setSeq] = useState(0);
  const active = files.some(f => f.progress < 100);
  useEffect(() => {
    if (!active) return;
    const iv = setInterval(() => {
      setFiles(fs => fs.map(f => f.progress < 100 ? {
        ...f,
        progress: Math.min(100, f.progress + 9)
      } : f));
    }, 110);
    return () => clearInterval(iv);
  }, [active]);
  const add = () => {
    const s = SAMPLES[seq % SAMPLES.length];
    setSeq(n => n + 1);
    setFiles(fs => [{
      id: seq,
      name: s.name,
      size: s.size,
      kind: s.kind,
      progress: 0
    }, ...fs].slice(0, 5));
  };
  const css = `
  .ud-root{--ud-bg:#FAF8F3;--ud-surface:rgba(0,0,0,0.025);--ud-border:rgba(0,0,0,0.09);--ud-text:#2b2722;--ud-dim:#6f6a62;--ud-faint:#8a8378;--ud-accent:#bf7551;--ud-track:rgba(0,0,0,0.07);border:1px solid var(--ud-border);border-radius:14px;background:var(--ud-bg);color:var(--ud-text);overflow:hidden;}
  .dark .ud-root{--ud-bg:#1b1a18;--ud-surface:rgba(255,255,255,0.03);--ud-border:rgba(255,255,255,0.08);--ud-text:#e7e3da;--ud-dim:#a8a299;--ud-faint:#8a8378;--ud-accent:#cf8a68;--ud-track:rgba(255,255,255,0.08);}
  .ud-zone{margin:16px;border:1.5px dashed var(--ud-border);border-radius:12px;padding:22px 16px;text-align:center;background:var(--ud-surface);}
  .ud-ic{font-size:24px;color:var(--ud-accent);opacity:.7;}
  .ud-zt{font-size:13.5px;color:var(--ud-dim);margin-top:7px;}
  .ud-btn{margin-top:11px;border:none;border-radius:9px;background:var(--ud-accent);color:#fff;font-size:13.5px;font-weight:600;padding:8px 18px;cursor:pointer;transition:opacity .15s;}
  .ud-btn:hover{opacity:.9;}
  .ud-hint{font-size:11.5px;color:var(--ud-faint);margin-top:9px;}
  .ud-list{padding:0 16px 14px;display:flex;flex-direction:column;gap:9px;}
  .ud-sec{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--ud-faint);padding:2px 2px 0;}
  .ud-empty{font-size:13px;color:var(--ud-faint);padding:4px 2px 8px;}
  .ud-file{border:1px solid var(--ud-border);border-radius:10px;padding:10px 13px;background:var(--ud-surface);}
  .ud-frow{display:flex;align-items:center;gap:10px;}
  .ud-fic{width:26px;height:26px;border-radius:7px;background:rgba(191,117,81,.12);color:var(--ud-accent);display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;}
  .ud-fname{flex:1 1 0;min-width:0;font-size:13.5px;font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
  .ud-fsize{font-size:12px;color:var(--ud-faint);flex-shrink:0;font-variant-numeric:tabular-nums;}
  .ud-fpct{font-size:12px;color:var(--ud-accent);flex-shrink:0;width:38px;text-align:right;font-variant-numeric:tabular-nums;}
  .ud-fdone{font-size:13px;color:#5f8a52;flex-shrink:0;}
  .dark .ud-fdone{color:#86b274;}
  .ud-bar{height:4px;border-radius:2px;background:var(--ud-track);overflow:hidden;margin-top:8px;}
  .ud-fill{height:100%;background:var(--ud-accent);border-radius:2px;transition:width .11s linear;}
  .ud-ic{line-height:0;}
  .ud-ic svg{width:30px;height:30px;}
  .ud-fic svg{width:15px;height:15px;}
  .ud-fdone{display:flex;align-items:center;}
  .ud-fdone svg{width:16px;height:16px;}
  `;
  const uploading = files.filter(f => f.progress < 100);
  const done = files.filter(f => f.progress >= 100);
  return <div className="ud-root">
      <style>{css}</style>
      <div className="ud-zone">
        <div className="ud-ic"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 13v8" /><path d="m8 17 4-4 4 4" /><path d="M20 16.2A4.5 4.5 0 0 0 17.5 8h-1.8A7 7 0 1 0 4 14.9" /></svg></div>
        <div className="ud-zt">{t.drop}</div>
        <button type="button" className="ud-btn" onClick={add}>{t.btn}</button>
        <div className="ud-hint">{t.hint}</div>
      </div>
      <div className="ud-list">
        {files.length === 0 && <div className="ud-empty">{t.empty}</div>}
        {uploading.length > 0 && <div className="ud-sec">{t.uploading}</div>}
        {uploading.map(f => <div className="ud-file" key={f.id}>
            <div className="ud-frow">
              <span className="ud-fic">{fileIcon(f.kind)}</span>
              <span className="ud-fname">{f.name}</span>
              <span className="ud-fsize">{f.size}</span>
              <span className="ud-fpct">{f.progress}%</span>
            </div>
            <div className="ud-bar"><div className="ud-fill" style={{
    width: f.progress + "%"
  }} /></div>
          </div>)}
        {done.length > 0 && <div className="ud-sec">{t.done}</div>}
        {done.map(f => <div className="ud-file" key={f.id}>
            <div className="ud-frow">
              <span className="ud-fic">{fileIcon(f.kind)}</span>
              <span className="ud-fname">{f.name}</span>
              <span className="ud-fsize">{f.size}</span>
              <span className="ud-fdone"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
            </div>
          </div>)}
      </div>
    </div>;
};

export const ArchFlow = ({nodes = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    flow: "Data flow",
    hint: "Click any stage to see what it does",
    stage: "Stage"
  } : {
    flow: "資料流",
    hint: "點任一階段看它做什麼",
    stage: "階段"
  };
  const safe = Array.isArray(nodes) ? nodes : [];
  const [active, setActive] = useState(0);
  if (safe.length === 0) return null;
  const idx = Math.min(active, safe.length - 1);
  const current = safe[idx];
  const css = `
  .af-root{--af-bg:#FAF8F3;--af-surface:rgba(191,117,81,0.06);--af-surface2:rgba(0,0,0,0.025);--af-border:rgba(0,0,0,0.09);--af-text:#2b2722;--af-dim:#6f6a62;--af-faint:#8a8378;--af-rail:rgba(0,0,0,0.12);--af-accent:#bf7551;border:1px solid var(--af-border);border-radius:14px;background:var(--af-bg);color:var(--af-text);overflow:hidden;}
  .dark .af-root{--af-bg:#1b1a18;--af-surface:rgba(207,138,104,0.09);--af-surface2:rgba(255,255,255,0.03);--af-border:rgba(255,255,255,0.08);--af-text:#e7e3da;--af-dim:#a8a299;--af-faint:#8a8378;--af-rail:rgba(255,255,255,0.13);--af-accent:#cf8a68;}
  .af-head{padding:13px 18px 11px;display:flex;align-items:center;gap:9px;flex-wrap:wrap;border-bottom:1px solid var(--af-border);}
  .af-head-ic{color:var(--af-accent);flex-shrink:0;}
  .af-head-t{font-size:12px;font-weight:700;letter-spacing:.6px;color:var(--af-dim);text-transform:uppercase;}
  .af-head-h{font-size:12px;color:var(--af-faint);}
  .af-grid{display:flex;gap:0;align-items:stretch;}
  .af-list{flex:1 1 0;min-width:0;padding:12px 12px 16px;}
  .af-node{display:flex;align-items:stretch;gap:10px;width:100%;text-align:left;background:transparent;border:none;cursor:pointer;padding:0;color:inherit;font:inherit;-webkit-tap-highlight-color:transparent;}
  .af-railcol{width:22px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;}
  .af-dot{width:9px;height:9px;border-radius:50%;margin-top:14px;background:var(--af-faint);opacity:.5;flex-shrink:0;transition:background .15s,box-shadow .15s,opacity .15s;}
  .af-line{width:2px;flex:1 1 0;background:var(--af-rail);min-height:10px;}
  .af-body{flex:1 1 0;min-width:0;padding:9px 12px;margin:2px 0;border-radius:9px;border:1px solid transparent;transition:background .15s,border-color .15s;}
  .af-node:hover .af-body{background:var(--af-surface2);}
  .af-node-on .af-body{background:var(--af-surface);border-color:rgba(191,117,81,.30);}
  .af-node-on .af-dot{background:var(--af-accent);opacity:1;box-shadow:0 0 0 4px rgba(191,117,81,.15);}
  .af-label{font-size:14.5px;font-weight:550;line-height:1.45;}
  .af-node-on .af-label{color:var(--af-accent);}
  .af-detail{width:300px;flex-shrink:0;border-left:1px solid var(--af-border);padding:16px 18px;background:var(--af-surface2);}
  .af-detail-k{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--af-accent);margin-bottom:7px;}
  .af-detail-l{font-size:16px;font-weight:600;margin-bottom:9px;line-height:1.35;}
  .af-detail-d{font-size:13.5px;line-height:1.65;color:var(--af-dim);}
  @media (max-width:640px){.af-grid{flex-direction:column;}.af-detail{width:auto;border-left:none;border-top:1px solid var(--af-border);}}
  `;
  return <div className="af-root">
      <style>{css}</style>
      <div className="af-head">
        <svg className="af-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="8" height="8" x="3" y="3" rx="2" /><path d="M7 11v4a2 2 0 0 0 2 2h4" /><rect width="8" height="8" x="13" y="13" rx="2" /></svg>
        <span className="af-head-t">{t.flow}</span>
        <span className="af-head-h">{t.hint}</span>
      </div>
      <div className="af-grid">
        <div className="af-list">
          {safe.map((n, i) => <button key={i} type="button" className={"af-node" + (i === idx ? " af-node-on" : "")} onClick={() => setActive(i)} onMouseEnter={() => setActive(i)}>
              <div className="af-railcol">
                <div className="af-dot" />
                {i < safe.length - 1 && <div className="af-line" />}
              </div>
              <div className="af-body">
                <div className="af-label">{n.label}</div>
              </div>
            </button>)}
        </div>
        <div className="af-detail">
          <div className="af-detail-k">{t.stage} {idx + 1} / {safe.length}</div>
          <div className="af-detail-l">{current.label}</div>
          <div className="af-detail-d">{current.detail}</div>
        </div>
      </div>
    </div>;
};

`Cloudflare Workers` · `Cloudflare R2` · `JavaScript` · `Serverless` · `Wrangler`

## Overview

A serverless file upload and management system using Cloudflare Workers as the compute layer and Cloudflare R2 as object storage. There is no file size limit by default, making it well suited to machine learning model weights (`.pt`, `.ckpt`, `.safetensors`, etc.). The frontend can be hosted on Cloudflare Pages, GitHub Pages, or opened locally as a static HTML file. The backend Worker is deployed and managed with the wrangler CLI.

## Interface preview

Click "Simulate upload" to feel the real drag-and-drop flow (this is a frontend mock and does not upload anything).

<UploadDemo lang="en" />

## Core features

* No file size limit; handles large model files without configuration changes
* Drag-and-drop upload interface with real-time progress and estimated time remaining
* Automatic file-type detection with matching icons
* Image preview and download for all file types
* File deletion from the management interface
* Cloudflare Workers handle upload, download, and delete API requests
* Flexible frontend hosting: Cloudflare Pages, GitHub Pages, or local static file

## Architecture

<ArchFlow
  lang="en"
  nodes={[
{ label: "Browser", detail: "The user performs drag-and-drop uploads, browsing, downloads, and deletions in the frontend page, which calls the Worker API endpoints via fetch." },
{ label: "Static frontend host", detail: "The frontend static files can be deployed to Cloudflare Pages, GitHub Pages, or opened locally as a static HTML file; no specific host is required." },
{ label: "Cloudflare Workers", detail: "A serverless function running at Cloudflare's edge that handles upload, download, and delete API requests and enforces CORS. Deployed with wrangler deploy." },
{ label: "Cloudflare R2", detail: "Object storage with no default file size limit, suited to large binary files such as model weights (.pt, .ckpt, .safetensors)." }
]}
/>

Frontend and backend are fully decoupled. The Worker is deployed to Cloudflare's edge with `wrangler deploy`; the frontend only needs the correct Worker URL and can be hosted anywhere.

## Quick start

<Steps>
  <Step title="Install wrangler CLI and log in">
    ```bash theme={null}
    npm install -g wrangler
    wrangler login
    ```
  </Step>

  <Step title="Create an R2 bucket">
    ```bash theme={null}
    wrangler r2 bucket create your-bucket-name
    ```
  </Step>

  <Step title="Configure wrangler.toml">
    Fill in the Worker name, R2 bucket binding, and bucket name:

    ```toml theme={null}
    name = "your-worker-name"
    main = "src/index.js"
    compatibility_date = "yyyy-MM-dd"

    [[r2_buckets]]
    binding = "your-binding"
    bucket_name = "your-bucket-name"

    [vars]
    ALLOWED_ORIGINS = "*"
    ```
  </Step>

  <Step title="Update the frontend API URL">
    In `frontend/app.js`, set `API_URL` to your Worker endpoint:

    ```javascript theme={null}
    const API_URL = 'https://your-worker-name.your-account.workers.dev';
    ```
  </Step>

  <Step title="Deploy the Worker">
    ```bash theme={null}
    wrangler deploy
    ```
  </Step>

  <Step title="Deploy or open the frontend">
    Upload the `frontend/` directory to Cloudflare Pages or GitHub Pages, or open `frontend/index.html` directly in a browser.
  </Step>
</Steps>

## Notes

<Warning>
  CORS is set to `*` by default and there is no authentication. Anyone who knows the Worker URL can upload, download, or delete files. Before using this in production, add an authentication layer and restrict CORS to specific origins. Without these changes the storage is effectively public. R2 usage may also incur Cloudflare charges; monitor your usage limits.
</Warning>

## In practice

A practical fit for individuals or small teams who need a place to store large binary files (model weights, dataset archives) without running a backend server. It also works as a hands-on starting point for learning Cloudflare Workers and R2 together.

## Links

* GitHub: [felimet/cf-auto-deploy-worker](https://github.com/felimet/cf-auto-deploy-worker)
