Skip to content

Resumable file upload #275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 30 additions & 30 deletions 5-network/09-resume-upload/article.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,82 @@
# Resumable file upload
# Upload del file ripristinabile

With `fetch` method it's fairly easy to upload a file.
Con il metodo `fetch` è abbastanza semplice eseguire l'upload di un file.

How to resume the upload after lost connection? There's no built-in option for that, but we have the pieces to implement it.
Come possiamo ripristinare l'upload di un file dopo avere perso la connessione? Non esistono opzioni built-in per questa operazione, ma abbiamo dei pezzi di codice per implementarlo.

Resumable uploads should come with upload progress indication, as we expect big files (if we may need to resume). So, as `fetch` doesn't allow to track upload progress, we'll use [XMLHttpRequest](info:xmlhttprequest).
Il ripristino degli upload dovrebbe andare a braccetto con la possibilità di tenerne traccia durante il trasferimento, come ci aspetteremmo per files di grosse dimensioni (se abbiamo bisogno di ripristinare l'operazione). Dal momento che `fetch` non permette di tenere traccia dell'upload, allora dobbiamo rifarci all'uso di [XMLHttpRequest](info:xmlhttprequest).

## Not-so-useful progress event
## Evento di progresso non-così-utile

To resume upload, we need to know how much was uploaded till the connection was lost.
Per ripristinare un upload, dobbiamo conoscere la quantità di dati trasferiti prima che la connessione si interrompesse.

There's `xhr.upload.onprogress` to track upload progress.
Per tenere traccia del progresso di upload possiamo usare `xhr.upload.onprogress`.

Unfortunately, it won't help us to resume the upload here, as it triggers when the data is *sent*, but was it received by the server? The browser doesn't know.
Sfortunatamente, questo non ci aiuta nel ripristinare l'upload, dal momento che questo evento viene scatenato solamente quando il dato è stato *inviato*. Ma è stato ricevuto dal server? Il browser non lo sa.

Maybe it was buffered by a local network proxy, or maybe the remote server process just died and couldn't process them, or it was just lost in the middle and didn't reach the receiver.
Magari potrebbe essere stato bufferizzato da qualche proxy di rete locale, o magari il processo del server remoto è stato terminato e non è più in grado di processarlo, oppure è stato perso nel bel mezzo del trasferimento e non raggiunge il ricevente.

That's why this event is only useful to show a nice progress bar.
Questo è il motivo per il quale la sua utilità si limita a mostrare una carinissima barra di caricamento.

To resume upload, we need to know *exactly* the number of bytes received by the server. And only the server can tell that, so we'll make an additional request.
Per ripristinare l'upload, abbiamo bisogno di conoscere *esattamente* il numero di bytes ricevuti dal server. E questa informazione può darcela solamente il server, motivo per il quale andiamo a creare una richiesta aggiuntiva.

## Algorithm
## Algoritmo

1. First, create a file id, to uniquely identify the file we're going to upload:
1. Per prima cosa, creiamo un id del file, per identificare univocamente ciò che stiamo andando a trasferire:
```js
let fileId = file.name + '-' + file.size + '-' + file.lastModified;
```
That's needed for resume upload, to tell the server what we're resuming.
Ciò è necessario per ripristinare l'upload, per dire al server cosa stiamo ripristinando.

If the name or the size or the last modification date changes, then there'll be another `fileId`.
Se il nome, la dimensione, oppure la data di ultima modifica sono differenti, allora ci sarà un `fileId` differente.

2. Send a request to the server, asking how many bytes it already has, like this:
2. Inviamo una richiesta al server, chiedendo quanti bytes possiede già di quel file:
```js
let response = await fetch('status', {
headers: {
'X-File-Id': fileId
}
});

// The server has that many bytes
// Il server possiede questo numero di bytes
let startByte = +await response.text();
```

This assumes that the server tracks file uploads by `X-File-Id` header. Should be implemented at server-side.
Questo presume che il server tenga traccia degli upload dei files tramite l'header `X-File-Id`. Dovrebbe essere implementato lato server.

If the file doesn't yet exist at the server, then the server response should be `0`
Se il file non esiste ancora nel server, il valore della risposta dovrebbe essere `0`

3. Then, we can use `Blob` method `slice` to send the file from `startByte`:
3. Quindi, possiamo usare il metodo `slice` di `Blob` per inviare il file partendo da `startByte`:
```js
xhr.open("POST", "upload", true);

// File id, so that the server knows which file we upload
// File id, in modo tale che il server possa sapere di quale file stiamo eseguendo l'upload
xhr.setRequestHeader('X-File-Id', fileId);

// The byte we're resuming from, so the server knows we're resuming
// Il byte a partire dal quale stiamo eseguendo il ripristino, in modo da consentire al server di sapere da che punto stiamo cominciando a ripristinare
xhr.setRequestHeader('X-Start-Byte', startByte);

xhr.upload.onprogress = (e) => {
console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
};

// file can be from input.files[0] or another source
// il file puo' provenire da input.files[0] o da altra fonte
xhr.send(file.slice(startByte));
```

Here we send the server both file id as `X-File-Id`, so it knows which file we're uploading, and the starting byte as `X-Start-Byte`, so it knows we're not uploading it initially, but resuming.
Qui inviamo al server sia il file id come `X-File-Id`, di modo che sappia quale file stiamo trasferendo, e da quale byte stiamo ripartendo tramite `X-Start-Byte`, cosicché sappia che non stiamo partendo dall'inizio, ma che, invece, stiamo ripristinando.

The server should check its records, and if there was an upload of that file, and the current uploaded size is exactly `X-Start-Byte`, then append the data to it.
Il server dovrebbe controllare i suoi registri, e nel caso in cui trovasse un upload del file, e la dimensione attualmente caricata fosse esattamente di `X-Start-Byte`, accoderebbe i dati al file.


Here's the demo with both client and server code, written on Node.js.
Ecco una demo con il codice client e la relativa parte server, scritta in Node.js.

It works only partially on this site, as Node.js is behind another server named Nginx, that buffers uploads, passing them to Node.js when fully complete.
Funziona parzialmente su questo sito, dal momento che Node.js sta su un altro server chiamato Nginx, che bufferizza gli uploads, passandoglieli solo a trasferimento completato.

But you can download it and run locally for the full demonstration:
È comunque possibile scaricare l'esempio ed eseguirlo in locale per la dimostrazione completa:

[codetabs src="upload-resume" height=200]

As we can see, modern networking methods are close to file managers in their capabilities -- control over headers, progress indicator, sending file parts, etc.
Come possiamo vedere, i moderni metodi di rete sono molto vicini all'essere dei gestori di files nelle loro capacità, controllo degli headers, indicazione del progresso di upload, invio di frammenti di files etc.

We can implement resumable upload and much more.
Possiamo implementare, quindi, upload ripristinabili e molto altro ancora.
14 changes: 7 additions & 7 deletions 5-network/09-resume-upload/upload-resume.view/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="myfile">
<input type="submit" name="submit" value="Upload (Resumes automatically)">
<input type="submit" name="submit" value="Upload (Ripristina automaticamente)">
</form>

<button onclick="uploader.stop()">Stop upload</button>
<button onclick="uploader.stop()">Interrompi upload</button>


<div id="log">Progress indication</div>
<div id="log">Indicatore del progresso di upload</div>

<script>
function log(html) {
Expand All @@ -19,7 +19,7 @@
}

function onProgress(loaded, total) {
log("progress " + loaded + ' / ' + total);
log("progresso " + loaded + ' / ' + total);
}

let uploader;
Expand All @@ -36,14 +36,14 @@
let uploaded = await uploader.upload();

if (uploaded) {
log('success');
log('completato con successo');
} else {
log('stopped');
log('interrotto');
}

} catch(err) {
console.error(err);
log('error');
log('errore');
}
};

Expand Down
22 changes: 11 additions & 11 deletions 5-network/09-resume-upload/upload-resume.view/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,36 @@ function onUpload(req, res) {
res.end();
}

// we'll files "nowhere"
// non salveremo "da nessuna parte"
let filePath = '/dev/null';
// could use a real path instead, e.g.
// invece potremmo usare un percorso reale, ad esempio
// let filePath = path.join('/tmp', fileId);

debug("onUpload fileId: ", fileId);

// initialize a new upload
// inizializza un nuovo upload
if (!uploads[fileId]) uploads[fileId] = {};
let upload = uploads[fileId];

debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte)

let fileStream;

// if startByte is 0 or not set, create a new file, otherwise check the size and append to existing one
// se startByte e' 0 o non e' impostato, crea un nuovo file, altrimenti controlla la dimensione del file e lo accoda a quello esistente
if (!startByte) {
upload.bytesReceived = 0;
fileStream = fs.createWriteStream(filePath, {
flags: 'w'
});
debug("New file created: " + filePath);
} else {
// we can check on-disk file size as well to be sure
// possiamo controllare su disco la dimensione del file per sicurezza
if (upload.bytesReceived != startByte) {
res.writeHead(400, "Wrong start byte");
res.end(upload.bytesReceived);
return;
}
// append to existing file
// accoda al file esistente
fileStream = fs.createWriteStream(filePath, {
flags: 'a'
});
Expand All @@ -59,26 +59,26 @@ function onUpload(req, res) {
upload.bytesReceived += data.length;
});

// send request body to file
// invia il corpo della richiesta al file
req.pipe(fileStream);

// when the request is finished, and all its data is written
// quando la richiesta è stata completata, e tutti i dati sono stati scritti
fileStream.on('close', function() {
if (upload.bytesReceived == req.headers['x-file-size']) {
debug("Upload finished");
delete uploads[fileId];

// can do something else with the uploaded file here
// qui puo' fare qualcos'altro con il file caricato

res.end("Success " + upload.bytesReceived);
} else {
// connection lost, we leave the unfinished file around
// connessione persa, lasciamo il file incompleto
debug("File unfinished, stopped at " + upload.bytesReceived);
res.end();
}
});

// in case of I/O error - finish the request
// in caso di errore I/O - conclude la richiesta
fileStream.on('error', function(err) {
debug("fileStream error");
res.writeHead(500, "File error");
Expand Down
16 changes: 8 additions & 8 deletions 5-network/09-resume-upload/upload-resume.view/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ class Uploader {
this.file = file;
this.onProgress = onProgress;

// create fileId that uniquely identifies the file
// we could also add user session identifier (if had one), to make it even more unique
// crea un fileId che identifica univocamente il file
// potremmo usare l'identificatore di sessione dell'utente (avendone uno) per essere ancora piu' sicuri della sua univocita'
this.fileId = file.name + '-' + file.size + '-' + file.lastModified;
}

Expand All @@ -31,9 +31,9 @@ class Uploader {
let xhr = this.xhr = new XMLHttpRequest();
xhr.open("POST", "upload", true);

// send file id, so that the server knows which file to resume
// invia il file id, in modo da consentire al server di conoscere quale file ripristinare
xhr.setRequestHeader('X-File-Id', this.fileId);
// send the byte we're resuming from, so the server knows we're resuming
// invia la posizione del byte dal quale stiamo partendo per il ripristino, in modo da informare il server da dove stiamo ripartendo
xhr.setRequestHeader('X-Start-Byte', this.startByte);

xhr.upload.onprogress = (e) => {
Expand All @@ -44,9 +44,9 @@ class Uploader {
xhr.send(this.file.slice(this.startByte));

// return
// true if upload was successful,
// false if aborted
// throw in case of an error
// true se l'upload e' andato a buon fine
// false se annullato
// throw in caso di errore
return await new Promise((resolve, reject) => {

xhr.onload = xhr.onerror = () => {
Expand All @@ -59,7 +59,7 @@ class Uploader {
}
};

// onabort triggers only when xhr.abort() is called
// onabort viene scatenato solo se viene chiamato xhr.abort()
xhr.onabort = () => resolve(false);

});
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy