Odeslat soubor metodou fetch je velice snadné.
Jak obnovit odeslání po ztrátě spojení? Neexistuje pro to žádná vestavěná možnost, ale máme součásti, které nám to umožní implementovat.
Obnovitelné odesílání by mělo přijít společně s oznamováním průběhu odesílání, protože očekáváme velké soubory (pokud máme potřebu je obnovovat). Jelikož fetch neumožňuje sledovat průběh odesílání, použijeme XMLHttpRequest.
Nepříliš užitečná událost průběhu
Abychom mohli odesílání obnovit, potřebujeme vědět, kolik bytů bylo odesláno před ztrátou spojení.
Pro sledování průběhu odesílání máme událost xhr.upload.onprogress.
Naneštěstí nám tady tato událost nepomůže obnovit odesílání, protože se spouští, když jsou data odeslána, ale byla přijata serverem? To prohlížeč neví.
Možná byla uložena do bufferu místní síťovou proxy, možná vzdálený serverový proces spadl a nemohl je zpracovat, nebo prostě byla ztracena po cestě a nedostala se k příjemci.
Tato událost je tedy užitečná jen k zobrazení hezkého ukazatele průběhu.
Abychom mohli obnovit odesílání, musíme znát přesný počet bytů, které server přijal. A ten nám může sdělit jedině server, proto vytvoříme dodatečný požadavek.
Algoritmus
-
Nejprve vytvoříme identifikátor souboru, aby unikátně identifikoval soubor, který se chystáme odeslat:
let idSouboru = soubor.name + '-' + soubor.size + '-' + soubor.lastModified;Ten je zapotřebí k obnově odesílání, abychom sdělili serveru, co obnovujeme.
Pokud se změní název, velikost nebo datum poslední změny, vytvoří se jiný
idSouboru. -
Pošleme na server požadavek s dotazem, kolik bytů už server má, například:
let odpověď = await fetch('status', { headers: { 'X-File-Id': fileId } }); // Server má tento počet bytů let počátečníByte = +await odpověď.text();Předpokládáme, že server sleduje odesílání souborů podle hlavičky
X-File-Id. To by mělo být implementováno na straně serveru.Jestliže soubor na serveru ještě neexistuje, odpověď serveru by měla být
0. -
Pak můžeme použít metodu
sliceobjektuBlobk odeslání souboru odpočátečníByte:xhr.open("POST", "upload"); // id souboru, aby server věděl, který soubor odesíláme xhr.setRequestHeader('X-File-Id', idSouboru); // byte, od kterého obnovujeme, aby server věděl, že obnovujeme xhr.setRequestHeader('X-Start-Byte', počátečníByte); xhr.upload.onprogress = (e) => { console.log(`Odesláno ${počátečníByte + e.loaded} z ${počátečníByte + e.total}`); }; // soubor může být z input.files[0] nebo jiného zdroje xhr.send(soubor.slice(počátečníByte));Zde posíláme serveru jak identifikátor souboru v hlavičce
X-File-Id, aby server věděl, který soubor odesíláme, tak počáteční byte v hlavičceX-Start-Byte, aby server věděl, že soubor neodesíláme od začátku, ale obnovujeme přerušené odesílání.Server by si měl projít své záznamy, a pokud tento soubor dříve přijímal a aktuální velikost přijatých dat je přesně
X-Start-Byte, měl by nová data připojit k němu.
Následuje demo s kódem klienta i serveru, napsané v Node.js.
Na této stránce funguje jen částečně, protože Node.js je za jiným serverem jménem Nginx, který si přijatá data ukládá do bufferu a předává je Node.js, až když jsou zcela kompletní.
Můžete si však demo stáhnout a pro plnou ukázku si je spustit lokálně:
let http = require('http');
let static = require('node-static');
let fileServer = new static.Server('.');
let path = require('path');
let fs = require('fs');
let debug = require('debug')('example:resume-upload');
let uploads = Object.create(null);
function onUpload(req, res) {
let fileId = req.headers['x-file-id'];
let startByte = +req.headers['x-start-byte'];
if (!fileId) {
res.writeHead(400, "No file id");
res.end();
}
// budeme ukládat soubory „nikam“
let filePath = '/dev/null';
// mohli bychom místo toho použít skutečnou cestu, např.
// let filePath = path.join('/tmp', fileId);
debug("onUpload fileId: ", fileId);
// inicializujeme nový příjem
if (!uploads[fileId]) uploads[fileId] = {};
let upload = uploads[fileId];
debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte)
let fileStream;
// pokud startByte je 0 nebo není nastaven, vytvoříme nový soubor, jinak zkontrolujeme velikost a připojíme data k existujícímu
if (!startByte) {
upload.bytesReceived = 0;
fileStream = fs.createWriteStream(filePath, {
flags: 'w'
});
debug("New file created: " + filePath);
} else {
// můžeme zkontrolovat i velikost souboru na disku, abychom měli jistotu
if (upload.bytesReceived != startByte) {
res.writeHead(400, "Wrong start byte");
res.end(upload.bytesReceived);
return;
}
// připojíme k existujícímu souboru
fileStream = fs.createWriteStream(filePath, {
flags: 'a'
});
debug("File reopened: " + filePath);
}
req.on('data', function(data) {
debug("bytes received", upload.bytesReceived);
upload.bytesReceived += data.length;
});
// pošleme do souboru tělo požadavku
req.pipe(fileStream);
// když je požadavek dokončen a všechna jeho data jsou zapsána
fileStream.on('close', function() {
if (upload.bytesReceived == req.headers['x-file-size']) {
debug("Upload finished");
delete uploads[fileId];
// s přijatým souborem zde můžeme dělat něco jiného
res.end("Success " + upload.bytesReceived);
} else {
// ztráta spojení, necháme soubor nedokončený
debug("File unfinished, stopped at " + upload.bytesReceived);
res.end();
}
});
// v případě I/O chyby ukončíme požadavek
fileStream.on('error', function(err) {
debug("fileStream error");
res.writeHead(500, "File error");
res.end();
});
}
function onStatus(req, res) {
let fileId = req.headers['x-file-id'];
let upload = uploads[fileId];
debug("onStatus fileId:", fileId, " upload:", upload);
if (!upload) {
res.end("0")
} else {
res.end(String(upload.bytesReceived));
}
}
function accept(req, res) {
if (req.url == '/status') {
onStatus(req, res);
} else if (req.url == '/upload' && req.method == 'POST') {
onUpload(req, res);
} else {
fileServer.serve(req, res);
}
}
// -----------------------------------
if (!module.parent) {
http.createServer(accept).listen(8080);
console.log('Server listening at port 8080');
} else {
exports.accept = accept;
}class Uploader {
constructor({soubor, onProgress}) {
this.soubor = soubor;
this.onProgress = onProgress;
// vytvoříme idSouboru, který unikátně identifikuje soubor
// můžeme také přidat identifikátor uživatelského připojení (pokud nějaké máme), aby byl ještě unikátnější
this.idSouboru = soubor.name + '-' + soubor.size + '-' + soubor.lastModified;
}
async vraťPočetOdeslanýchBytů() {
let odpověď = await fetch('status', {
headers: {
'X-File-Id': this.idSouboru
}
});
if (odpověď.status != 200) {
throw new Error("Nelze získat počet odeslaných bytů: " + odpověď.statusText);
}
let text = await odpověď.text();
return +text;
}
async upload() {
this.počátečníByte = await this.vraťPočetOdeslanýchBytů();
let xhr = this.xhr = new XMLHttpRequest();
xhr.open("POST", "upload", true);
// pošleme id souboru, aby server věděl, který soubor má obnovit
xhr.setRequestHeader('X-File-Id', this.idSouboru);
// pošleme byte, od kterého obnovujeme, aby server věděl, že obnovujeme
xhr.setRequestHeader('X-Start-Byte', this.počátečníByte);
xhr.upload.onprogress = (e) => {
this.onProgress(this.počátečníByte + e.loaded, this.počátečníByte + e.total);
};
console.log("posíláme soubor, začínáme od", this.počátečníByte);
xhr.send(this.soubor.slice(this.počátečníByte));
// vrátíme
// true, pokud bylo odeslání úspěšné
// false, pokud bylo přerušeno
// throw v případě chyby
return await new Promise((splň, zamítni) => {
xhr.onload = xhr.onerror = () => {
console.log("upload end status:" + xhr.status + " text:" + xhr.statusText);
if (xhr.status == 200) {
splň(true);
} else {
zamítni(new Error("Odeslání selhalo: " + xhr.statusText));
}
};
// onabort se spustí jen při volání xhr.abort()
xhr.onabort = () => resolve(false);
});
}
stop() {
if (this.xhr) {
this.xhr.abort();
}
}
}<!DOCTYPE HTML>
<script src="uploader.js"></script>
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="myfile">
<input type="submit" name="submit" value="Odeslat (Automaticky se obnoví)">
</form>
<button onclick="uploader.stop()">Konec odesílání</button>
<div id="log">Zobrazení průběhu</div>
<script>
function log(html) {
document.getElementById('log').innerHTML = html;
console.log(html);
}
function onProgress(načteno, celkem) {
log("progress " + načteno + ' / ' + celkem);
}
let uploader;
document.forms.upload.onsubmit = async function(e) {
e.preventDefault();
let soubor = this.elements.myfile.files[0];
if (!soubor) return;
uploader = new Uploader({soubor, onProgress});
try {
let odesláno = await uploader.upload();
if (odesláno) {
log('úspěch');
} else {
log('přerušeno');
}
} catch(err) {
console.error(err);
log('chyba');
}
};
</script>Jak vidíme, moderní síťové metody se svými možnostmi blíží správcům souborů – máme kontrolu nad hlavičkami, indikátor průběhu, posílání částí souborů a podobně.
Můžeme implementovat obnovitelné odesílání a mnoho dalšího.
Komentáře
<code>, pro několik řádků je obalte značkou<pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)