A big file, in pieces
Watch it go up chunk by chunk, then pull the plug halfway and send it again — it picks up where it left off.
<div class="demo demo--wide">
<input id="big" name="big" type="file">
<div class="demo-row">
<button class="demo-btn" id="send">Send a 12 MB file</button>
<button class="demo-btn demo-btn--ghost" id="drop">Pull the plug</button>
<button class="demo-btn demo-btn--ghost" id="again">Send it again</button>
</div>
<dl class="demo-kv" id="log"></dl>
<p class="demo-hint">The "server" is a few lines running in this page: it writes each piece at the
offset it was given and answers a <code>HEAD</code> with how much it holds.</p>
</div>
import { createFileUploader, formatBytes } from 'https://devix.pk/cdn/oss/file-uploader@1.0.0/index.js';
const log = document.getElementById('log');
const CHUNK = 2_000_000;
const SIZE = 12_000_000;
// A receiver, in the page: the whole protocol is an offset and a write.
const stored = { bytes: 0, broken: false };
const realOpen = XMLHttpRequest.prototype.open;
const realSend = XMLHttpRequest.prototype.send;
const realHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function open(method, url, ...rest) {
this._demo = { method, url: String(url), headers: {} };
return realOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(name, value) {
if (this._demo?.url.includes('/demo-upload')) { this._demo.headers[name.toLowerCase()] = value; return; }
return realHeader.call(this, name, value);
};
XMLHttpRequest.prototype.send = function send(body) {
if (!this._demo?.url.includes('/demo-upload')) return realSend.call(this, body);
const { method, headers } = this._demo;
const reply = (status, text, responseHeaders = {}) => {
Object.defineProperty(this, 'status', { value: status, configurable: true });
Object.defineProperty(this, 'responseText', { value: text, configurable: true });
this.getResponseHeader = (name) => responseHeaders[name.toLowerCase()] ?? null;
setTimeout(() => {
if (body && this.upload) this.upload.dispatchEvent(Object.assign(new Event('progress'), { lengthComputable: true, loaded: body.size ?? 0, total: body.size ?? 0 }));
this.dispatchEvent(new Event('loadend'));
}, 180);
};
if (method === 'HEAD') { reply(200, '', { 'x-uploaded-bytes': String(stored.bytes) }); return; }
const offset = Number(headers['x-chunk-offset'] ?? 0);
const index = Number(headers['x-chunk-index'] ?? 0);
const count = Number(headers['x-chunk-count'] ?? 1);
if (stored.broken && offset >= SIZE / 2) { reply(0, ''); return; }
stored.bytes = Math.max(stored.bytes, offset + (body?.size ?? 0));
note(`piece ${index + 1} of ${count}`, `${formatBytes(stored.bytes)} held`);
reply(200, JSON.stringify({ ok: true, held: stored.bytes }));
};
function note(term, value) {
const row = document.createElement('div');
row.innerHTML = `<dt>${term}</dt><dd>${value}</dd>`;
log.append(row);
log.scrollTop = log.scrollHeight;
}
const uploader = createFileUploader(document.getElementById('big'), {
url: '/demo-upload',
chunkSize: CHUNK,
autoUpload: true,
retries: 0,
onSuccess: () => note('done', 'the server has the whole file'),
onError: (file, message) => note('stopped', message),
});
const file = () => new File([new Uint8Array(SIZE)], 'recording.mp4', { type: 'video/mp4' });
document.getElementById('send').addEventListener('click', () => {
stored.bytes = 0;
stored.broken = false;
log.textContent = '';
uploader.clear();
uploader.addFiles([file()]);
});
document.getElementById('drop').addEventListener('click', () => {
stored.broken = true;
note('the connection', 'dropped');
});
document.getElementById('again').addEventListener('click', () => {
stored.broken = false;
note('sending again', `the server already holds ${formatBytes(stored.bytes)}`);
uploader.clear();
uploader.addFiles([file()]);
});
@import url('https://devix.pk/cdn/oss/file-uploader@1.0.0/file-uploader.min.css');
#log { display: block; max-height: 150px; overflow-y: auto; }
#log div { display: flex; justify-content: space-between; gap: 12px; padding: 5px 9px; }
#log:empty { display: none; }