Starchik commited on
Commit
0f151de
·
verified ·
1 Parent(s): 270d713

Upload 2260 files

Browse files
Files changed (3) hide show
  1. public/js/api.js +66 -18
  2. public/js/app.js +19 -5
  3. public/js/share.js +6 -3
public/js/api.js CHANGED
@@ -16,24 +16,69 @@ const API = (() => {
16
  }
17
  const res = await fetch(base + url, { method, headers, body: payload });
18
  if (!res.ok) {
19
- let msg = 'Ошибка запроса';
20
- try { msg = (await res.json()).error || msg; } catch (e) {}
21
- // сессия истекла или недействительна — не оставляем пользователя
22
- // молча биться в стену, а отправляем обратно на вход
23
- if (res.status === 401 && !url.startsWith('/webauthn/') && !url.startsWith('/auth/login') && !url.startsWith('/auth/register') && !url.startsWith('/share/')) {
24
- sessionStorage.clear();
25
- if (!location.pathname.endsWith('index.html') && !location.pathname.endsWith('share.html')) {
26
- location.href = 'index.html?expired=1';
27
- }
28
- }
29
- const err = new Error(msg);
30
- err.status = res.status;
31
- throw err;
32
  }
33
  const ct = res.headers.get('content-type') || '';
34
  if (ct.includes('application/json')) return res.json();
35
  return res.arrayBuffer();
36
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  return {
38
  register: (username, password) => req('POST', '/auth/register', { username, password }),
39
  login: (username, password) => req('POST', '/auth/login', { username, password }),
@@ -44,9 +89,9 @@ const API = (() => {
44
  deleteFolder: (id) => req('DELETE', `/folders/${id}`),
45
  rubbish: () => req('GET', '/rubbish'),
46
  emptyRubbish: () => req('POST', '/rubbish/empty', {}),
47
- async uploadFile(formData) {
48
- return req('POST', '/files/upload', formData, { form: true });
49
- },
50
  fileMeta: (id) => req('GET', `/files/${id}/meta`),
51
  downloadFile: (id) => req('GET', `/files/${id}/download`),
52
  patchFile: (id, body) => req('PATCH', `/files/${id}`, body),
@@ -59,8 +104,11 @@ const API = (() => {
59
  shareMeta: (publicId) => req('GET', `/share/${publicId}/meta`),
60
  shareUnlock: (publicId, verifier) => req('POST', `/share/${publicId}/unlock`, { verifier }),
61
  // верификатор идёт заголовком, а не в URL: так он не попадёт в логи прокси
62
- shareDownload: (publicId, verifier) =>
63
- req('GET', `/share/${publicId}/download`, undefined, verifier ? { headers: { 'x-share-verifier': verifier } } : {}),
 
 
 
64
  // WebAuthn / биометрия
65
  webauthnRegisterOptions: () => req('POST', '/webauthn/register-options', {}),
66
  webauthnRegisterVerify: (attestationResponse, deviceLabel) => req('POST', '/webauthn/register-verify', { attestationResponse, deviceLabel }),
 
16
  }
17
  const res = await fetch(base + url, { method, headers, body: payload });
18
  if (!res.ok) {
19
+ let msg = '';
20
+ try { msg = (await res.json()).error || ''; } catch (e) {}
21
+ throw failure(res.status, url, msg);
 
 
 
 
 
 
 
 
 
 
22
  }
23
  const ct = res.headers.get('content-type') || '';
24
  if (ct.includes('application/json')) return res.json();
25
  return res.arrayBuffer();
26
  }
27
+
28
+ // Разбор неуспешного ответа, общий для fetch- и XHR-путей.
29
+ function failure(status, url, msg) {
30
+ // сессия истекла или недействительна — не оставляем пользователя
31
+ // молча биться в стену, а отправляем обратно на вход
32
+ if (status === 401 && !url.startsWith('/webauthn/') && !url.startsWith('/auth/login') && !url.startsWith('/auth/register') && !url.startsWith('/share/')) {
33
+ sessionStorage.clear();
34
+ if (!location.pathname.endsWith('index.html') && !location.pathname.endsWith('share.html')) {
35
+ location.href = 'index.html?expired=1';
36
+ }
37
+ }
38
+ const err = new Error(msg || 'Ошибка запроса');
39
+ err.status = status;
40
+ return err;
41
+ }
42
+
43
+ // fetch() не сообщает, сколько байт тела уже ушло на сервер, поэтому запросы
44
+ // с полосой прогресса идут через XHR — только ради событий progress.
45
+ // onUploadProgress/onDownloadProgress получают долю 0..1 и вызываются лишь
46
+ // когда известен полный размер; без них запрос ведёт себя как обычный.
47
+ function reqWithProgress(method, url, body, opts = {}) {
48
+ return new Promise((resolve, reject) => {
49
+ const xhr = new XMLHttpRequest();
50
+ xhr.open(method, base + url);
51
+ xhr.responseType = 'arraybuffer';
52
+ const t = token();
53
+ if (t) xhr.setRequestHeader('Authorization', 'Bearer ' + t);
54
+ for (const [k, v] of Object.entries(opts.headers || {})) xhr.setRequestHeader(k, v);
55
+ let payload = body;
56
+ if (body && !opts.form) {
57
+ xhr.setRequestHeader('Content-Type', 'application/json');
58
+ payload = JSON.stringify(body);
59
+ }
60
+ const track = (target, cb) => {
61
+ if (cb) target.onprogress = (e) => { if (e.lengthComputable && e.total) cb(e.loaded / e.total); };
62
+ };
63
+ track(xhr.upload, opts.onUploadProgress);
64
+ track(xhr, opts.onDownloadProgress);
65
+ xhr.onload = () => {
66
+ const text = () => new TextDecoder().decode(xhr.response || new ArrayBuffer(0));
67
+ if (xhr.status < 200 || xhr.status > 299) {
68
+ let msg = '';
69
+ try { msg = JSON.parse(text()).error || ''; } catch (e) {}
70
+ return reject(failure(xhr.status, url, msg));
71
+ }
72
+ if ((xhr.getResponseHeader('content-type') || '').includes('application/json')) {
73
+ try { return resolve(JSON.parse(text())); } catch (e) { return reject(new Error('Некорректный ответ сервера')); }
74
+ }
75
+ resolve(xhr.response);
76
+ };
77
+ xhr.onerror = () => reject(new Error('Нет связи с сервером'));
78
+ xhr.onabort = () => reject(new Error('Загрузка отменена'));
79
+ xhr.send(payload === undefined ? null : payload);
80
+ });
81
+ }
82
  return {
83
  register: (username, password) => req('POST', '/auth/register', { username, password }),
84
  login: (username, password) => req('POST', '/auth/login', { username, password }),
 
89
  deleteFolder: (id) => req('DELETE', `/folders/${id}`),
90
  rubbish: () => req('GET', '/rubbish'),
91
  emptyRubbish: () => req('POST', '/rubbish/empty', {}),
92
+ // onProgress получает долю отправленных байт (0..1)
93
+ uploadFile: (formData, onProgress) =>
94
+ reqWithProgress('POST', '/files/upload', formData, { form: true, onUploadProgress: onProgress }),
95
  fileMeta: (id) => req('GET', `/files/${id}/meta`),
96
  downloadFile: (id) => req('GET', `/files/${id}/download`),
97
  patchFile: (id, body) => req('PATCH', `/files/${id}`, body),
 
104
  shareMeta: (publicId) => req('GET', `/share/${publicId}/meta`),
105
  shareUnlock: (publicId, verifier) => req('POST', `/share/${publicId}/unlock`, { verifier }),
106
  // верификатор идёт заголовком, а не в URL: так он не попадёт в логи прокси
107
+ shareDownload: (publicId, verifier, onProgress) =>
108
+ reqWithProgress('GET', `/share/${publicId}/download`, undefined, {
109
+ headers: verifier ? { 'x-share-verifier': verifier } : {},
110
+ onDownloadProgress: onProgress,
111
+ }),
112
  // WebAuthn / биометрия
113
  webauthnRegisterOptions: () => req('POST', '/webauthn/register-options', {}),
114
  webauthnRegisterVerify: (attestationResponse, deviceLabel) => req('POST', '/webauthn/register-verify', { attestationResponse, deviceLabel }),
public/js/app.js CHANGED
@@ -1002,7 +1002,13 @@ async function handleUpload(fileList) {
1002
  const fill = row.querySelector('.progress-fill');
1003
  const pctEl = row.querySelector('.fname span:last-child');
1004
  try {
1005
- await uploadOne(file, (pct) => { fill.style.width = pct + '%'; pctEl.textContent = pct + '%'; });
 
 
 
 
 
 
1006
  fill.style.width = '100%'; pctEl.innerHTML = icon('check', 'icon-sm');
1007
  } catch (e) {
1008
  pctEl.textContent = 'Ошибка';
@@ -1014,15 +1020,21 @@ async function handleUpload(fileList) {
1014
  load(); refreshQuota();
1015
  }
1016
 
 
 
 
 
 
 
 
1017
  async function uploadOne(file, onProgress) {
1018
  const fileKey = await VLT.generateFileKey();
1019
  const buffer = await file.arrayBuffer();
1020
- onProgress(20);
1021
  const { ciphertext, iv } = await VLT.encryptBuffer(fileKey, buffer);
1022
- onProgress(45);
1023
  const { data: attrsEncrypted, iv: attrsIv } = await VLT.encryptJson(fileKey, { name: file.name, type: file.type });
1024
  const { keyWrapped, keyWrapIv } = await VLT.wrapFileKey(masterKey, fileKey);
1025
- onProgress(60);
1026
 
1027
  const form = new FormData();
1028
  form.append('blob', new Blob([ciphertext]), 'blob');
@@ -1034,7 +1046,9 @@ async function uploadOne(file, onProgress) {
1034
  form.append('keyWrapped', keyWrapped);
1035
  form.append('keyWrapIv', keyWrapIv);
1036
 
1037
- await API.uploadFile(form);
 
 
1038
  onProgress(100);
1039
  }
1040
 
 
1002
  const fill = row.querySelector('.progress-fill');
1003
  const pctEl = row.querySelector('.fname span:last-child');
1004
  try {
1005
+ // события прогресса сыпятся часто — перерисовываем только на смене процента
1006
+ let shown = -1;
1007
+ await uploadOne(file, (pct) => {
1008
+ if (pct === shown) return;
1009
+ shown = pct;
1010
+ fill.style.width = pct + '%'; pctEl.textContent = pct + '%';
1011
+ });
1012
  fill.style.width = '100%'; pctEl.innerHTML = icon('check', 'icon-sm');
1013
  } catch (e) {
1014
  pctEl.textContent = 'Ошибка';
 
1020
  load(); refreshQuota();
1021
  }
1022
 
1023
+ // Вехи полосы загрузки. Чтение и шифрование быстрые и от сети не зависят,
1024
+ // поэтому им отдано только начало шкалы; всё остальное — отправка, где прогресс
1025
+ // приходит от XHR по фактически ушедшим байтам. При отправке до 100% не
1026
+ // дотягиваем: последние проценты закрывает ответ сервера, иначе полоса
1027
+ // показывала бы «готово», пока сервер ещё принимает файл.
1028
+ const UP_READ = 4, UP_ENCRYPTED = 12, UP_SENT = 97;
1029
+
1030
  async function uploadOne(file, onProgress) {
1031
  const fileKey = await VLT.generateFileKey();
1032
  const buffer = await file.arrayBuffer();
1033
+ onProgress(UP_READ);
1034
  const { ciphertext, iv } = await VLT.encryptBuffer(fileKey, buffer);
 
1035
  const { data: attrsEncrypted, iv: attrsIv } = await VLT.encryptJson(fileKey, { name: file.name, type: file.type });
1036
  const { keyWrapped, keyWrapIv } = await VLT.wrapFileKey(masterKey, fileKey);
1037
+ onProgress(UP_ENCRYPTED);
1038
 
1039
  const form = new FormData();
1040
  form.append('blob', new Blob([ciphertext]), 'blob');
 
1046
  form.append('keyWrapped', keyWrapped);
1047
  form.append('keyWrapIv', keyWrapIv);
1048
 
1049
+ await API.uploadFile(form, (ratio) => {
1050
+ onProgress(UP_ENCRYPTED + Math.round(ratio * (UP_SENT - UP_ENCRYPTED)));
1051
+ });
1052
  onProgress(100);
1053
  }
1054
 
public/js/share.js CHANGED
@@ -134,10 +134,13 @@ async function showFile(publicId, meta, fileKey) {
134
  const progLine = document.getElementById('progLine');
135
  const progFill = document.getElementById('progFill');
136
  btn.disabled = true; btn.innerHTML = `${icon('download', 'icon-sm')} Скачивание…`;
137
- progLine.style.display = 'block'; progFill.style.width = '30%';
138
  try {
139
- const buf = await API.shareDownload(publicId, shareVerifier);
140
- progFill.style.width = '70%';
 
 
 
141
  const plain = await VLT.decryptBuffer(fileKey, VLT.b64.toBuf(meta.contentIv), buf);
142
  progFill.style.width = '100%';
143
  const blob = new Blob([plain], { type: attrs.type || 'application/octet-stream' });
 
134
  const progLine = document.getElementById('progLine');
135
  const progFill = document.getElementById('progFill');
136
  btn.disabled = true; btn.innerHTML = `${icon('download', 'icon-sm')} Скачивание…`;
137
+ progLine.style.display = 'block'; progFill.style.width = '0%';
138
  try {
139
+ // полоса идёт по фактически полученным байтам; остаток шкалы оставлен
140
+ // на расшифровку, она начинается только после полной загрузки
141
+ const buf = await API.shareDownload(publicId, shareVerifier, (ratio) => {
142
+ progFill.style.width = (ratio * 92).toFixed(1) + '%';
143
+ });
144
  const plain = await VLT.decryptBuffer(fileKey, VLT.b64.toBuf(meta.contentIv), buf);
145
  progFill.style.width = '100%';
146
  const blob = new Blob([plain], { type: attrs.type || 'application/octet-stream' });