14. dubna 2026

Obnovitelné odesílání souboru

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

  1. 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.

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

  3. Pak můžeme použít metodu slice objektu Blob k odeslání souboru od počá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čce X-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ě:

Výsledek
server.js
uploader.js
index.html
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.

Mapa tutoriálu

Komentáře

přečtěte si před komentováním…
  • Máte-li návrhy na zlepšení, vytvořte prosím issue na GitHubu nebo pull request místo komentáře.
  • Pokud v článku něčemu nerozumíte, napište prosím, čemu přesně a na kterém místě.
  • Pro vložení několika slov kódu použijte značku <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…)