Skip to content

Commit 39a2426

Browse files
Merge pull request #290 from xcurveballx/5-network/09-resume-upload
5 network/09 resume upload
2 parents d8909dd + 213bc4c commit 39a2426

4 files changed

Lines changed: 49 additions & 49 deletions

File tree

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,75 @@
1-
# Resumable file upload
1+
# Возобновляемая загрузка файлов
22

3-
With `fetch` method it's fairly easy to upload a file.
3+
С помощью метода `fetch` можно достаточно просто загрузить файл на сервер.
44

5-
How to resume the upload after lost connection? There's no built-in option for that, but we have the pieces to implement it.
5+
Но как возобновить загрузку, если соединение прервалось? Для этого не предусмотрено какого-то встроенного способа, но у нас есть все средства, чтобы решить эту задачу самостоятельно.
66

7-
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).
7+
Возобновляемая загрузка должна сопровождаться индикацией прогресса, так как мы ожидаем, что могут загружаться большие по весу файлы. Поскольку `fetch` не позволяет отслеживать прогресс загрузки, то мы будем использовать [XMLHttpRequest](info:xmlhttprequest).
88

9-
## Not-so-useful progress event
9+
## Не очень полезное событие progress
1010

11-
To resume upload, we need to know how much was uploaded till the connection was lost.
11+
Чтобы возобновить загрузку, нам нужно знать, какая часть файла была успешно передана до того, как соединение прервалось.
1212

13-
There's `xhr.upload.onprogress` to track upload progress.
13+
Можно установить обработчик `xhr.upload.onprogress`, чтобы отслеживать процесс загрузки.
1414

15-
Unfortunately, it's useless here, as it triggers when the data is *sent*, but was it received by the server? The browser doesn't know.
15+
К сожалению, это бесполезно, так как этот обработчик вызывается, только когда данные *отправляются*, но были ли они получены сервером? Браузер этого не знает.
1616

17-
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 when the connection broke, and didn't reach the receiver.
17+
Возможно, отправленные данные оказались в буфере прокси-сервера локальной сети или удалённый сервер просто отключился и не смог принять их, или данные потерялись где-то по пути при разрыве соединения и так и не достигли пункта назначения.
1818

19-
So, this event is only useful to show a nice progress bar.
19+
Таким образом, событие `progress` подходит только для того, чтобы показывать красивый индикатор загрузки, не более.
2020

21-
To resume upload, we need to know exactly the number of bytes received by the server. And only the server can tell that.
21+
Для возобновления же загрузки нужно точно знать, сколько байт было получено сервером. И только сам сервер может поделиться с нами этой информацией.
2222

23-
## Algorithm
23+
## Алгоритм
2424

25-
1. First, we create a file id, to uniquely identify the file we're uploading, e.g.
25+
1. Во-первых, давайте присвоим загружаемому файлу уникальный идентификатор
2626
```js
2727
let fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
2828
```
29-
That's needed for resume upload, to tell the server what we're resuming.
29+
Это нужно, чтобы при возобновлении загрузки серверу было понятно, какой файл мы продолжаем загружать.
3030

31-
2. Send a request to the server, asking how many bytes it already has, like this:
31+
2. Далее, посылаем запрос к серверу с просьбой указать количество уже полученных байтов:
3232
```js
3333
let response = await fetch('status', {
3434
headers: {
3535
'X-File-Id': fileId
3636
}
3737
});
3838
39-
// The server has that many bytes
39+
// сервер получил столько-то байтов
4040
let startByte = +await response.text();
4141
```
4242

43-
This assumes that the server tracks file uploads by `X-File-Id` header. Should be implemented at server-side.
43+
Предполагается, что сервер учитывает загружаемые файлы с помощью заранее настроенного заголовка `X-File-Id`.
4444

45-
3. Then, we can use `Blob` method `slice` to send the file from `startByte`:
45+
3. Затем мы можем использовать метод `slice` объекта `Blob`, чтобы отправить данные, начиная со `startByte` байта:
4646
```js
4747
xhr.open("POST", "upload", true);
4848
49-
// send file id, so that the server knows which file to resume
49+
// отправка идентификатора файла, чтобы сервер знал, загрузку чего мы потом возобновим
5050
xhr.setRequestHeader('X-File-Id', fileId);
51-
// send the byte we're resuming from, so the server knows we're resuming
51+
// отправка номера байта, начиная с которого мы будем отправлять данные. Таким образом, сервер поймёт, что мы возобновляем загрузку
5252
xhr.setRequestHeader('X-Start-Byte', startByte);
5353
5454
xhr.upload.onprogress = (e) => {
5555
console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
5656
};
5757
58-
// file can be from input.files[0] or another source
58+
// файл может быть взят из input.files[0] или другого источника
5959
xhr.send(file.slice(startByte));
6060
```
6161

62-
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.
62+
Здесь мы посылаем серверу и идентификатор файла в заголовке `X-File-Id`, чтобы он знал, что мы загружаем, и номер стартового байта в заголовке `X-Start-Byte`, чтобы он понял, что мы продолжаем закачку, а не начинаем её с нуля.
6363

64-
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.
64+
Сервер должен проверить информацию на своей стороне, и если обнаружится, что такой файл уже когда-то загружался, и его текущий размер равен значению из заголовка `X-Start-Byte`, то вновь принимаемые данные добавлять в этот файл.
6565

6666

67-
Here's the demo with both client and server code, written on Node.js.
67+
Ниже представлен демо-код как для сервера (Node.js), так и для клиента.
6868

69-
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.
69+
Пример работает только частично на этом сайте, так как Node.js здесь располагается за другим веб-сервером Nginx, который сохраняет в своём буфере все загружаемые файлы и передаёт их дальше в Node.js только после завершения загрузки.
7070

71-
But you can download it and run locally for the full demonstration:
71+
Но вы можете скачать код и запустить его локально, чтобы увидеть полный пример в действии:
7272

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

75-
As you can see, modern networking methods are close to file managers in their capabilities -- control over headers, progress indicator, sending file parts, etc.
75+
Как вы видите, современные методы работы с сетью очень близки по своим возможностям к файловым менеджерам -- контроль заголовков, индикация прогресса загрузки, отправка данных по частям и так далее.

5-network/09-resume-upload/upload-resume.view/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
66
<input type="file" name="myfile">
7-
<input type="submit" name="submit" value="Upload (Resumes automatically)">
7+
<input type="submit" name="submit" value="Загрузить (возобновляется автоматически)">
88
</form>
99

10-
<button onclick="uploader.stop()">Stop upload</button>
10+
<button onclick="uploader.stop()">Остановить загрузку</button>
1111

1212

1313
<div id="log">Progress indication</div>

5-network/09-resume-upload/upload-resume.view/server.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,23 @@ function onUpload(req, res) {
1717
res.end();
1818
}
1919

20-
// we'll files "nowhere"
20+
// мы будем сохранять файлы "в никуда"
2121
let filePath = '/dev/null';
22-
// could use a real path instead, e.g.
22+
// хотя могли бы использовать реальный путь, например
2323
// let filePath = path.join('/tmp', fileId);
2424

2525
debug("onUpload fileId: ", fileId);
2626

27-
// initialize a new upload
27+
// инициируем новую загрузку
2828
if (!uploads[fileId]) uploads[fileId] = {};
2929
let upload = uploads[fileId];
3030

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

3333
let fileStream;
3434

35-
// for byte 0, create a new file, otherwise check the size and append to existing one
36-
if (startByte == 0) {
35+
// если стартовый байт 0 или не указан - создаём новый файл, иначе проверяем размер и добавляем данные к уже существующему файлу
36+
if (!startByte) {
3737
upload.bytesReceived = 0;
3838
fileStream = fs.createWriteStream(filePath, {
3939
flags: 'w'
@@ -45,7 +45,7 @@ function onUpload(req, res) {
4545
res.end(upload.bytesReceived);
4646
return;
4747
}
48-
// append to existing file
48+
// добавить к существующему файлу
4949
fileStream = fs.createWriteStream(filePath, {
5050
flags: 'a'
5151
});
@@ -58,26 +58,26 @@ function onUpload(req, res) {
5858
upload.bytesReceived += data.length;
5959
});
6060

61-
// send request body to file
61+
// сохранить тело запроса в файл
6262
req.pipe(fileStream);
6363

64-
// when the request is finished, and all its data is written
64+
// когда обработка запроса завершена и все данные записаны
6565
fileStream.on('close', function() {
6666
if (upload.bytesReceived == req.headers['x-file-size']) {
6767
debug("Upload finished");
6868
delete uploads[fileId];
6969

70-
// can do something else with the uploaded file here
70+
// мы можем сделать что-то ещё с загруженным файлом
7171

7272
res.end("Success " + upload.bytesReceived);
7373
} else {
74-
// connection lost, we leave the unfinished file around
74+
// соединение потеряно, остаются незавершённые загрузки
7575
debug("File unfinished, stopped at " + upload.bytesReceived);
7676
res.end();
7777
}
7878
});
7979

80-
// in case of I/O error - finish the request
80+
// в случае ошибки ввода/вывода завершаем обработку запроса
8181
fileStream.on('error', function(err) {
8282
debug("fileStream error");
8383
res.writeHead(500, "File error");

5-network/09-resume-upload/upload-resume.view/uploader.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ class Uploader {
44
this.file = file;
55
this.onProgress = onProgress;
66

7-
// create fileId that uniquely identifies the file
8-
// we can also add user session identifier, to make it even more unique
7+
// создаём уникальный идентификатор файла
8+
// для большей уникальности мы также могли бы добавить идентификатор пользовательской сессии (если она есть)
99
this.fileId = file.name + '-' + file.size + '-' + +file.lastModifiedDate;
1010
}
1111

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

34-
// send file id, so that the server knows which file to resume
34+
// отправляем индентификатор файла, так что сервер будет знать, какая из загрузок возобновляется
3535
xhr.setRequestHeader('X-File-Id', this.fileId);
36-
// send the byte we're resuming from, so the server knows we're resuming
36+
// отправляем номер байта, с которого следует возобновить загрузку, то есть сервер поймёт, что это не новая загрузка
3737
xhr.setRequestHeader('X-Start-Byte', this.startByte);
3838

3939
xhr.upload.onprogress = (e) => {
@@ -43,10 +43,10 @@ class Uploader {
4343
console.log("send the file, starting from", this.startByte);
4444
xhr.send(this.file.slice(this.startByte));
4545

46-
// return
47-
// true if upload was successful,
48-
// false if aborted
49-
// throw in case of an error
46+
// возвращаем
47+
// true, если загрузка успешно завершилась
48+
// false, если она отменена
49+
// выбрасываем исключение в случае ошибки
5050
return await new Promise((resolve, reject) => {
5151

5252
xhr.onload = xhr.onerror = () => {
@@ -59,7 +59,7 @@ class Uploader {
5959
}
6060
};
6161

62-
// onabort triggers only when xhr.abort() is called
62+
// этот обработчик срабатывает, только когда вызывается xhr.abort()
6363
xhr.onabort = () => resolve(false);
6464

6565
});

0 commit comments

Comments
 (0)