Merge pull request #868 from mozilla/multifile
Implemented multi-file upload/download
This commit is contained in:
commit
ebbb174c66
28
app/api.js
28
app/api.js
|
@ -1,4 +1,5 @@
|
||||||
import { arrayToB64, b64ToArray, delay } from './utils';
|
import { arrayToB64, b64ToArray, delay } from './utils';
|
||||||
|
import { ECE_RECORD_SIZE } from './ece';
|
||||||
|
|
||||||
function post(obj) {
|
function post(obj) {
|
||||||
return {
|
return {
|
||||||
|
@ -78,7 +79,8 @@ export async function metadata(id, keychain) {
|
||||||
ttl: data.ttl,
|
ttl: data.ttl,
|
||||||
iv: meta.iv,
|
iv: meta.iv,
|
||||||
name: meta.name,
|
name: meta.name,
|
||||||
type: meta.type
|
type: meta.type,
|
||||||
|
manifest: meta.manifest
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(result.response.status);
|
throw new Error(result.response.status);
|
||||||
|
@ -126,14 +128,7 @@ function listenForResponse(ws, canceller) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload(
|
async function upload(stream, metadata, verifierB64, onprogress, canceller) {
|
||||||
stream,
|
|
||||||
streamInfo,
|
|
||||||
metadata,
|
|
||||||
verifierB64,
|
|
||||||
onprogress,
|
|
||||||
canceller
|
|
||||||
) {
|
|
||||||
const host = window.location.hostname;
|
const host = window.location.hostname;
|
||||||
const port = window.location.port;
|
const port = window.location.port;
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
@ -166,10 +161,10 @@ async function upload(
|
||||||
|
|
||||||
ws.send(buf);
|
ws.send(buf);
|
||||||
|
|
||||||
onprogress([size, streamInfo.fileSize]);
|
onprogress(size);
|
||||||
size += buf.length;
|
size += buf.length;
|
||||||
state = await reader.read();
|
state = await reader.read();
|
||||||
while (ws.bufferedAmount > streamInfo.recordSize * 2) {
|
while (ws.bufferedAmount > ECE_RECORD_SIZE * 2) {
|
||||||
await delay();
|
await delay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +180,7 @@ async function upload(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadWs(encrypted, info, metadata, verifierB64, onprogress) {
|
export function uploadWs(encrypted, metadata, verifierB64, onprogress) {
|
||||||
const canceller = { cancelled: false };
|
const canceller = { cancelled: false };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -193,14 +188,7 @@ export function uploadWs(encrypted, info, metadata, verifierB64, onprogress) {
|
||||||
canceller.error = new Error(0);
|
canceller.error = new Error(0);
|
||||||
canceller.cancelled = true;
|
canceller.cancelled = true;
|
||||||
},
|
},
|
||||||
result: upload(
|
result: upload(encrypted, metadata, verifierB64, onprogress, canceller)
|
||||||
encrypted,
|
|
||||||
info,
|
|
||||||
metadata,
|
|
||||||
verifierB64,
|
|
||||||
onprogress,
|
|
||||||
canceller
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { blobStream, concatStream } from './streams';
|
||||||
|
|
||||||
|
export default class Archive {
|
||||||
|
constructor(files) {
|
||||||
|
this.files = Array.from(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.files.length > 1 ? 'Send-Archive.zip' : this.files[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this.files.length > 1 ? 'send-archive' : this.files[0].type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.files.reduce((total, file) => total + file.size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get manifest() {
|
||||||
|
return {
|
||||||
|
files: this.files.map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get stream() {
|
||||||
|
return concatStream(this.files.map(file => blobStream(file)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
export default class BlobSlicer {
|
|
||||||
constructor(blob, size) {
|
|
||||||
this.blob = blob;
|
|
||||||
this.index = 0;
|
|
||||||
this.chunkSize = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
pull(controller) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const bytesLeft = this.blob.size - this.index;
|
|
||||||
if (bytesLeft <= 0) {
|
|
||||||
controller.close();
|
|
||||||
return resolve();
|
|
||||||
}
|
|
||||||
const size = Math.min(this.chunkSize, bytesLeft);
|
|
||||||
const blob = this.blob.slice(this.index, this.index + size);
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
controller.enqueue(new Uint8Array(reader.result));
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsArrayBuffer(blob);
|
|
||||||
this.index += size;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* global MAXFILESIZE */
|
/* global MAXFILESIZE */
|
||||||
const { bytes } = require('./utils');
|
import Archive from './archive';
|
||||||
|
import { bytes } from './utils';
|
||||||
|
|
||||||
export default function(state, emitter) {
|
export default function(state, emitter) {
|
||||||
emitter.on('DOMContentLoaded', () => {
|
emitter.on('DOMContentLoaded', () => {
|
||||||
|
@ -18,11 +19,8 @@ export default function(state, emitter) {
|
||||||
if (target.files.length === 0) {
|
if (target.files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (target.files.length > 1) {
|
const file = new Archive(target.files);
|
||||||
// eslint-disable-next-line no-alert
|
|
||||||
return alert(state.translate('uploadPageMultipleFilesAlert'));
|
|
||||||
}
|
|
||||||
const file = target.files[0];
|
|
||||||
if (file.size === 0) {
|
if (file.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
67
app/ece.js
67
app/ece.js
|
@ -1,5 +1,4 @@
|
||||||
import 'buffer';
|
import 'buffer';
|
||||||
import BlobSlicer from './blobSlicer';
|
|
||||||
import { transformStream } from './streams';
|
import { transformStream } from './streams';
|
||||||
|
|
||||||
const NONCE_LENGTH = 12;
|
const NONCE_LENGTH = 12;
|
||||||
|
@ -7,7 +6,7 @@ const TAG_LENGTH = 16;
|
||||||
const KEY_LENGTH = 16;
|
const KEY_LENGTH = 16;
|
||||||
const MODE_ENCRYPT = 'encrypt';
|
const MODE_ENCRYPT = 'encrypt';
|
||||||
const MODE_DECRYPT = 'decrypt';
|
const MODE_DECRYPT = 'decrypt';
|
||||||
const RS = 1024 * 64;
|
export const ECE_RECORD_SIZE = 1024 * 64;
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
@ -282,52 +281,34 @@ class StreamSlicer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function encryptedSize(size, rs = ECE_RECORD_SIZE) {
|
||||||
|
return 21 + size + 16 * Math.floor(size / (rs - 17));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
input: a blob or a ReadableStream containing data to be transformed
|
input: a ReadableStream containing data to be transformed
|
||||||
key: Uint8Array containing key of size KEY_LENGTH
|
key: Uint8Array containing key of size KEY_LENGTH
|
||||||
mode: string, either 'encrypt' or 'decrypt'
|
|
||||||
rs: int containing record size, optional
|
rs: int containing record size, optional
|
||||||
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
|
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
|
||||||
*/
|
*/
|
||||||
export default class ECE {
|
export function encryptStream(
|
||||||
constructor(input, key, mode, rs, salt) {
|
input,
|
||||||
this.input = input;
|
key,
|
||||||
this.key = key;
|
rs = ECE_RECORD_SIZE,
|
||||||
this.mode = mode;
|
salt = generateSalt(KEY_LENGTH)
|
||||||
this.rs = rs;
|
) {
|
||||||
this.salt = salt;
|
const mode = 'encrypt';
|
||||||
if (rs === undefined) {
|
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
|
||||||
this.rs = RS;
|
return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
|
||||||
}
|
|
||||||
if (salt === undefined) {
|
|
||||||
this.salt = generateSalt(KEY_LENGTH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info() {
|
/*
|
||||||
return {
|
input: a ReadableStream containing data to be transformed
|
||||||
recordSize: this.rs,
|
key: Uint8Array containing key of size KEY_LENGTH
|
||||||
fileSize:
|
rs: int containing record size, optional
|
||||||
21 + this.input.size + 16 * Math.floor(this.input.size / (this.rs - 17))
|
*/
|
||||||
};
|
export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
|
||||||
}
|
const mode = 'decrypt';
|
||||||
|
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
|
||||||
transform() {
|
return transformStream(inputStream, new ECETransformer(mode, key, rs));
|
||||||
let inputStream;
|
|
||||||
|
|
||||||
if (this.input instanceof Blob) {
|
|
||||||
inputStream = new ReadableStream(
|
|
||||||
new BlobSlicer(this.input, this.rs - 17)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
inputStream = transformStream(
|
|
||||||
this.input,
|
|
||||||
new StreamSlicer(this.rs, this.mode)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return transformStream(
|
|
||||||
inputStream,
|
|
||||||
new ECETransformer(this.mode, this.key, this.rs, this.salt)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ export default class FileReceiver extends Nanobus {
|
||||||
this.fileInfo.type = meta.type;
|
this.fileInfo.type = meta.type;
|
||||||
this.fileInfo.iv = meta.iv;
|
this.fileInfo.iv = meta.iv;
|
||||||
this.fileInfo.size = meta.size;
|
this.fileInfo.size = meta.size;
|
||||||
|
this.fileInfo.manifest = meta.manifest;
|
||||||
this.state = 'ready';
|
this.state = 'ready';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +106,7 @@ export default class FileReceiver extends Nanobus {
|
||||||
id: this.fileInfo.id,
|
id: this.fileInfo.id,
|
||||||
filename: this.fileInfo.name,
|
filename: this.fileInfo.name,
|
||||||
type: this.fileInfo.type,
|
type: this.fileInfo.type,
|
||||||
|
manifest: this.fileInfo.manifest,
|
||||||
key: this.fileInfo.secretKey,
|
key: this.fileInfo.secretKey,
|
||||||
requiresPassword: this.fileInfo.requiresPassword,
|
requiresPassword: this.fileInfo.requiresPassword,
|
||||||
password: this.fileInfo.password,
|
password: this.fileInfo.password,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import OwnedFile from './ownedFile';
|
||||||
import Keychain from './keychain';
|
import Keychain from './keychain';
|
||||||
import { arrayToB64, bytes } from './utils';
|
import { arrayToB64, bytes } from './utils';
|
||||||
import { uploadWs } from './api';
|
import { uploadWs } from './api';
|
||||||
|
import { encryptedSize } from './ece';
|
||||||
|
|
||||||
export default class FileSender extends Nanobus {
|
export default class FileSender extends Nanobus {
|
||||||
constructor(file) {
|
constructor(file) {
|
||||||
|
@ -64,21 +65,15 @@ export default class FileSender extends Nanobus {
|
||||||
}
|
}
|
||||||
this.msg = 'encryptingFile';
|
this.msg = 'encryptingFile';
|
||||||
this.emit('encrypting');
|
this.emit('encrypting');
|
||||||
|
const totalSize = encryptedSize(this.file.size);
|
||||||
const enc = await this.keychain.encryptStream(this.file);
|
const encStream = await this.keychain.encryptStream(this.file.stream);
|
||||||
const metadata = await this.keychain.encryptMetadata(this.file);
|
const metadata = await this.keychain.encryptMetadata(this.file);
|
||||||
const authKeyB64 = await this.keychain.authKeyB64();
|
const authKeyB64 = await this.keychain.authKeyB64();
|
||||||
|
|
||||||
this.uploadRequest = uploadWs(
|
this.uploadRequest = uploadWs(encStream, metadata, authKeyB64, p => {
|
||||||
enc.stream,
|
this.progress = [p, totalSize];
|
||||||
enc.streamInfo,
|
|
||||||
metadata,
|
|
||||||
authKeyB64,
|
|
||||||
p => {
|
|
||||||
this.progress = p;
|
|
||||||
this.emit('progress');
|
this.emit('progress');
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (this.cancelled) {
|
if (this.cancelled) {
|
||||||
throw new Error(0);
|
throw new Error(0);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { arrayToB64, b64ToArray } from './utils';
|
import { arrayToB64, b64ToArray } from './utils';
|
||||||
import ECE from './ece.js';
|
import { decryptStream, encryptStream } from './ece.js';
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
@ -173,24 +173,20 @@ export default class Keychain {
|
||||||
iv: arrayToB64(this.iv),
|
iv: arrayToB64(this.iv),
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
size: metadata.size,
|
size: metadata.size,
|
||||||
type: metadata.type || 'application/octet-stream'
|
type: metadata.type || 'application/octet-stream',
|
||||||
|
manifest: metadata.manifest || {}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return ciphertext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptStream(plaintext) {
|
encryptStream(plainStream) {
|
||||||
const ece = new ECE(plaintext, this.rawSecret, 'encrypt');
|
return encryptStream(plainStream, this.rawSecret);
|
||||||
return {
|
|
||||||
stream: ece.transform(),
|
|
||||||
streamInfo: ece.info()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptStream(cryptotext) {
|
decryptStream(cryptotext) {
|
||||||
const ece = new ECE(cryptotext, this.rawSecret, 'decrypt');
|
return decryptStream(cryptotext, this.rawSecret);
|
||||||
return ece.transform();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptFile(ciphertext) {
|
async decryptFile(ciphertext) {
|
||||||
|
|
|
@ -33,6 +33,7 @@ module.exports = function(state, emit) {
|
||||||
<input id="file-upload"
|
<input id="file-upload"
|
||||||
class="inputFile"
|
class="inputFile"
|
||||||
type="file"
|
type="file"
|
||||||
|
multiple
|
||||||
name="fileUploaded"
|
name="fileUploaded"
|
||||||
onfocus=${onfocus}
|
onfocus=${onfocus}
|
||||||
onblur=${onblur}
|
onblur=${onblur}
|
||||||
|
@ -67,8 +68,10 @@ module.exports = function(state, emit) {
|
||||||
|
|
||||||
async function upload(event) {
|
async function upload(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
const Archive = require('../../archive').default;
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
const file = target.files[0];
|
const file = new Archive(target.files);
|
||||||
|
|
||||||
if (file.size === 0) {
|
if (file.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Keychain from './keychain';
|
import Keychain from './keychain';
|
||||||
import { downloadStream } from './api';
|
import { downloadStream } from './api';
|
||||||
import { transformStream } from './streams';
|
import { transformStream } from './streams';
|
||||||
|
import Zip from './zip';
|
||||||
import contentDisposition from 'content-disposition';
|
import contentDisposition from 'content-disposition';
|
||||||
|
|
||||||
let noSave = false;
|
let noSave = false;
|
||||||
|
@ -20,6 +21,8 @@ async function decryptStream(id) {
|
||||||
return new Response(null, { status: 400 });
|
return new Response(null, { status: 400 });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
let size = file.size;
|
||||||
|
let type = file.type;
|
||||||
const keychain = new Keychain(file.key, file.nonce);
|
const keychain = new Keychain(file.key, file.nonce);
|
||||||
if (file.requiresPassword) {
|
if (file.requiresPassword) {
|
||||||
keychain.setPassword(file.password, file.url);
|
keychain.setPassword(file.password, file.url);
|
||||||
|
@ -30,8 +33,16 @@ async function decryptStream(id) {
|
||||||
const body = await file.download.result;
|
const body = await file.download.result;
|
||||||
|
|
||||||
const decrypted = keychain.decryptStream(body);
|
const decrypted = keychain.decryptStream(body);
|
||||||
|
|
||||||
|
let zipStream = null;
|
||||||
|
if (file.type === 'send-archive') {
|
||||||
|
const zip = new Zip(file.manifest, decrypted);
|
||||||
|
zipStream = zip.stream;
|
||||||
|
type = 'application/zip';
|
||||||
|
size = zip.size;
|
||||||
|
}
|
||||||
const readStream = transformStream(
|
const readStream = transformStream(
|
||||||
decrypted,
|
zipStream || decrypted,
|
||||||
{
|
{
|
||||||
transform(chunk, controller) {
|
transform(chunk, controller) {
|
||||||
file.progress += chunk.length;
|
file.progress += chunk.length;
|
||||||
|
@ -48,8 +59,8 @@ async function decryptStream(id) {
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Disposition': contentDisposition(file.filename),
|
'Content-Disposition': contentDisposition(file.filename),
|
||||||
'Content-Type': file.type,
|
'Content-Type': type,
|
||||||
'Content-Length': file.size
|
'Content-Length': size
|
||||||
};
|
};
|
||||||
return new Response(readStream, { headers });
|
return new Response(readStream, { headers });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -81,6 +92,7 @@ self.onmessage = event => {
|
||||||
password: event.data.password,
|
password: event.data.password,
|
||||||
url: event.data.url,
|
url: event.data.url,
|
||||||
type: event.data.type,
|
type: event.data.type,
|
||||||
|
manifest: event.data.manifest,
|
||||||
size: event.data.size,
|
size: event.data.size,
|
||||||
progress: 0
|
progress: 0
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/* global ReadableStream TransformStream */
|
/* global ReadableStream TransformStream */
|
||||||
|
|
||||||
export function transformStream(readable, transformer, oncancel) {
|
export function transformStream(readable, transformer, oncancel) {
|
||||||
if (typeof TransformStream === 'function') {
|
try {
|
||||||
return readable.pipeThrough(new TransformStream(transformer));
|
return readable.pipeThrough(new TransformStream(transformer));
|
||||||
}
|
} catch (e) {
|
||||||
const reader = readable.getReader();
|
const reader = readable.getReader();
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
|
@ -38,3 +38,66 @@ export function transformStream(readable, transformer, oncancel) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlobStreamController {
|
||||||
|
constructor(blob, size) {
|
||||||
|
this.blob = blob;
|
||||||
|
this.index = 0;
|
||||||
|
this.chunkSize = size || 1024 * 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
pull(controller) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const bytesLeft = this.blob.size - this.index;
|
||||||
|
if (bytesLeft <= 0) {
|
||||||
|
controller.close();
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
const size = Math.min(this.chunkSize, bytesLeft);
|
||||||
|
const slice = this.blob.slice(this.index, this.index + size);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
controller.enqueue(new Uint8Array(reader.result));
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(slice);
|
||||||
|
this.index += size;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function blobStream(blob, size) {
|
||||||
|
return new ReadableStream(new BlobStreamController(blob, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConcatStreamController {
|
||||||
|
constructor(streams) {
|
||||||
|
this.streams = streams;
|
||||||
|
this.index = 0;
|
||||||
|
this.reader = null;
|
||||||
|
this.nextReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextReader() {
|
||||||
|
const next = this.streams[this.index++];
|
||||||
|
this.reader = next && next.getReader();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pull(controller) {
|
||||||
|
if (!this.reader) {
|
||||||
|
return controller.close();
|
||||||
|
}
|
||||||
|
const data = await this.reader.read();
|
||||||
|
if (data.done) {
|
||||||
|
this.nextReader();
|
||||||
|
return this.pull(controller);
|
||||||
|
}
|
||||||
|
controller.enqueue(data.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function concatStream(streams) {
|
||||||
|
return new ReadableStream(new ConcatStreamController(streams));
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
import crc32 from 'crc/crc32';
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
function dosDateTime(dateTime = new Date()) {
|
||||||
|
const year = (dateTime.getFullYear() - 1980) << 9;
|
||||||
|
const month = (dateTime.getMonth() + 1) << 5;
|
||||||
|
const day = dateTime.getDate();
|
||||||
|
const date = year | month | day;
|
||||||
|
const hour = dateTime.getHours() << 11;
|
||||||
|
const minute = dateTime.getMinutes() << 5;
|
||||||
|
const second = Math.floor(dateTime.getSeconds() / 2);
|
||||||
|
const time = hour | minute | second;
|
||||||
|
|
||||||
|
return { date, time };
|
||||||
|
}
|
||||||
|
|
||||||
|
class File {
|
||||||
|
constructor(info) {
|
||||||
|
this.name = encoder.encode(info.name);
|
||||||
|
this.size = info.size;
|
||||||
|
this.bytesRead = 0;
|
||||||
|
this.crc = null;
|
||||||
|
this.dateTime = dosDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
get header() {
|
||||||
|
const h = new ArrayBuffer(30 + this.name.byteLength);
|
||||||
|
const v = new DataView(h);
|
||||||
|
v.setUint32(0, 0x04034b50, true); // sig
|
||||||
|
v.setUint16(4, 20, true); // version
|
||||||
|
v.setUint16(6, 8, true); // bit flags (8 = use data descriptor)
|
||||||
|
v.setUint16(8, 0, true); // compression
|
||||||
|
v.setUint16(10, this.dateTime.time, true); // modified time
|
||||||
|
v.setUint16(12, this.dateTime.date, true); // modified date
|
||||||
|
v.setUint32(14, 0, true); // crc32 (in descriptor)
|
||||||
|
v.setUint32(18, 0, true); // compressed size (in descriptor)
|
||||||
|
v.setUint32(22, 0, true); // uncompressed size (in descriptor)
|
||||||
|
v.setUint16(26, this.name.byteLength, true); // name length
|
||||||
|
v.setUint16(28, 0, true); // extra field length
|
||||||
|
for (let i = 0; i < this.name.byteLength; i++) {
|
||||||
|
v.setUint8(30 + i, this.name[i]);
|
||||||
|
}
|
||||||
|
return new Uint8Array(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dataDescriptor() {
|
||||||
|
const dd = new ArrayBuffer(16);
|
||||||
|
const v = new DataView(dd);
|
||||||
|
v.setUint32(0, 0x08074b50, true); // sig
|
||||||
|
v.setUint32(4, this.crc, true); // crc32
|
||||||
|
v.setUint32(8, this.size, true); // compressed size
|
||||||
|
v.setUint16(12, this.size, true); // uncompressed size
|
||||||
|
return new Uint8Array(dd);
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryRecord(offset) {
|
||||||
|
const dr = new ArrayBuffer(46 + this.name.byteLength);
|
||||||
|
const v = new DataView(dr);
|
||||||
|
v.setUint32(0, 0x02014b50, true); // sig
|
||||||
|
v.setUint16(4, 20, true); // version made
|
||||||
|
v.setUint16(6, 20, true); // version required
|
||||||
|
v.setUint16(8, 0, true); // bit flags
|
||||||
|
v.setUint16(10, 0, true); // compression
|
||||||
|
v.setUint16(12, this.dateTime.time, true); // modified time
|
||||||
|
v.setUint16(14, this.dateTime.date, true); // modified date
|
||||||
|
v.setUint32(16, this.crc, true); // crc
|
||||||
|
v.setUint32(20, this.size, true); // compressed size
|
||||||
|
v.setUint32(24, this.size, true); // uncompressed size
|
||||||
|
v.setUint16(28, this.name.byteLength, true); // name length
|
||||||
|
v.setUint16(30, 0, true); // extra length
|
||||||
|
v.setUint16(32, 0, true); // comment length
|
||||||
|
v.setUint16(34, 0, true); // disk number
|
||||||
|
v.setUint16(36, 0, true); // internal file attrs
|
||||||
|
v.setUint32(38, 0, true); // external file attrs
|
||||||
|
v.setUint32(42, offset, true); // file offset
|
||||||
|
for (let i = 0; i < this.name.byteLength; i++) {
|
||||||
|
v.setUint8(46 + i, this.name[i]);
|
||||||
|
}
|
||||||
|
return new Uint8Array(dr);
|
||||||
|
}
|
||||||
|
|
||||||
|
get byteLength() {
|
||||||
|
return this.size + this.name.byteLength + 30 + 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(data, controller) {
|
||||||
|
this.bytesRead += data.byteLength;
|
||||||
|
const endIndex = data.byteLength - Math.max(this.bytesRead - this.size, 0);
|
||||||
|
const buf = data.slice(0, endIndex);
|
||||||
|
this.crc = crc32(buf, this.crc);
|
||||||
|
controller.enqueue(buf);
|
||||||
|
if (endIndex < data.byteLength) {
|
||||||
|
return data.slice(endIndex, data.byteLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function centralDirectory(files, controller) {
|
||||||
|
let directoryOffset = 0;
|
||||||
|
let directorySize = 0;
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const record = file.directoryRecord(directoryOffset);
|
||||||
|
directoryOffset += file.byteLength;
|
||||||
|
controller.enqueue(record);
|
||||||
|
directorySize += record.byteLength;
|
||||||
|
}
|
||||||
|
controller.enqueue(eod(files.length, directorySize, directoryOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
function eod(fileCount, directorySize, directoryOffset) {
|
||||||
|
const e = new ArrayBuffer(22);
|
||||||
|
const v = new DataView(e);
|
||||||
|
v.setUint32(0, 0x06054b50, true); // sig
|
||||||
|
v.setUint16(4, 0, true); // disk number
|
||||||
|
v.setUint16(6, 0, true); // directory disk
|
||||||
|
v.setUint16(8, fileCount, true); // number of records
|
||||||
|
v.setUint16(10, fileCount, true); // total records
|
||||||
|
v.setUint32(12, directorySize, true); // size of directory
|
||||||
|
v.setUint32(16, directoryOffset, true); // offset of directory
|
||||||
|
v.setUint16(20, 0, true); // comment length
|
||||||
|
return new Uint8Array(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZipStreamController {
|
||||||
|
constructor(files, source) {
|
||||||
|
this.files = files;
|
||||||
|
this.fileIndex = 0;
|
||||||
|
this.file = null;
|
||||||
|
this.reader = source.getReader();
|
||||||
|
this.nextFile();
|
||||||
|
this.extra = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFile() {
|
||||||
|
this.file = this.files[this.fileIndex++];
|
||||||
|
}
|
||||||
|
|
||||||
|
async pull(controller) {
|
||||||
|
if (!this.file) {
|
||||||
|
// end of archive
|
||||||
|
centralDirectory(this.files, controller);
|
||||||
|
return controller.close();
|
||||||
|
}
|
||||||
|
if (this.file.bytesRead === 0) {
|
||||||
|
// beginning of file
|
||||||
|
controller.enqueue(this.file.header);
|
||||||
|
if (this.extra) {
|
||||||
|
this.extra = this.file.append(this.extra, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.file.bytesRead >= this.file.size) {
|
||||||
|
// end of file
|
||||||
|
controller.enqueue(this.file.dataDescriptor);
|
||||||
|
this.nextFile();
|
||||||
|
return this.pull(controller);
|
||||||
|
}
|
||||||
|
const data = await this.reader.read();
|
||||||
|
if (data.done) {
|
||||||
|
this.nextFile();
|
||||||
|
return this.pull(controller);
|
||||||
|
}
|
||||||
|
this.extra = this.file.append(data.value, controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Zip {
|
||||||
|
constructor(manifest, source) {
|
||||||
|
this.files = manifest.files.map(info => new File(info));
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
get stream() {
|
||||||
|
return new ReadableStream(new ZipStreamController(this.files, this.source));
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
const entries = this.files.reduce(
|
||||||
|
(total, file) => total + file.byteLength * 2 - file.size,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const eod = 22;
|
||||||
|
return entries + eod;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2788,6 +2788,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crc": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc/-/crc-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-ZwmUex488OBjSVOMxnR/dIa1yxisBMJNEi+UxzXpKhax8MPsQtoRQtl5Qgo+W7pcSVkRXa3BEVjaniaWKtvKvw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"buffer": "5.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-YkIRgwsZwJWTnyQrsBTWefizHh+8GYj3kbL1BTiAQ/9pwpino0G7B2gp5tx/FUBqUlvtxV85KNR3mwfAtv15Yw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"base64-js": "1.3.0",
|
||||||
|
"ieee754": "1.1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"create-ecdh": {
|
"create-ecdh": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
"base64-js": "^1.3.0",
|
"base64-js": "^1.3.0",
|
||||||
"content-disposition": "^0.5.2",
|
"content-disposition": "^0.5.2",
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.2",
|
||||||
|
"crc": "^3.7.0",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"css-loader": "^1.0.0",
|
"css-loader": "^1.0.0",
|
||||||
"css-mqpacker": "^6.0.2",
|
"css-mqpacker": "^6.0.2",
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import Archive from '../../../app/archive';
|
||||||
import * as api from '../../../app/api';
|
import * as api from '../../../app/api';
|
||||||
import Keychain from '../../../app/keychain';
|
import Keychain from '../../../app/keychain';
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const plaintext = new Blob([encoder.encode('hello world!')]);
|
const plaintext = new Archive([new Blob([encoder.encode('hello world!')])]);
|
||||||
const metadata = {
|
const metadata = {
|
||||||
name: 'test.txt',
|
name: 'test.txt',
|
||||||
type: 'text/plain'
|
type: 'text/plain'
|
||||||
|
@ -13,11 +14,11 @@ describe('API', function() {
|
||||||
describe('websocket upload', function() {
|
describe('websocket upload', function() {
|
||||||
it('returns file info on success', async function() {
|
it('returns file info on success', async function() {
|
||||||
const keychain = new Keychain();
|
const keychain = new Keychain();
|
||||||
const enc = await keychain.encryptStream(plaintext);
|
const enc = await keychain.encryptStream(plaintext.stream);
|
||||||
const meta = await keychain.encryptMetadata(metadata);
|
const meta = await keychain.encryptMetadata(metadata);
|
||||||
const verifierB64 = await keychain.authKeyB64();
|
const verifierB64 = await keychain.authKeyB64();
|
||||||
const p = function() {};
|
const p = function() {};
|
||||||
const up = api.uploadWs(enc.stream, enc.streamInfo, meta, verifierB64, p);
|
const up = api.uploadWs(enc, meta, verifierB64, p);
|
||||||
|
|
||||||
const result = await up.result;
|
const result = await up.result;
|
||||||
assert.ok(result.url);
|
assert.ok(result.url);
|
||||||
|
@ -27,11 +28,11 @@ describe('API', function() {
|
||||||
|
|
||||||
it('can be cancelled', async function() {
|
it('can be cancelled', async function() {
|
||||||
const keychain = new Keychain();
|
const keychain = new Keychain();
|
||||||
const enc = await keychain.encryptStream(plaintext);
|
const enc = await keychain.encryptStream(plaintext.stream);
|
||||||
const meta = await keychain.encryptMetadata(metadata);
|
const meta = await keychain.encryptMetadata(metadata);
|
||||||
const verifierB64 = await keychain.authKeyB64();
|
const verifierB64 = await keychain.authKeyB64();
|
||||||
const p = function() {};
|
const p = function() {};
|
||||||
const up = api.uploadWs(enc.stream, enc.streamInfo, meta, verifierB64, p);
|
const up = api.uploadWs(enc, meta, verifierB64, p);
|
||||||
up.cancel();
|
up.cancel();
|
||||||
try {
|
try {
|
||||||
await up.result;
|
await up.result;
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import FileSender from '../../../app/fileSender';
|
import FileSender from '../../../app/fileSender';
|
||||||
|
import Archive from '../../../app/archive';
|
||||||
|
|
||||||
// FileSender uses a File in real life but a Blob works for testing
|
// FileSender uses a File in real life but a Blob works for testing
|
||||||
const blob = new Blob(['hello world!'], { type: 'text/plain' });
|
const blob = new Blob(['hello world!'], { type: 'text/plain' });
|
||||||
blob.name = 'text.txt';
|
blob.name = 'text.txt';
|
||||||
|
const archive = new Archive([blob]);
|
||||||
|
|
||||||
describe('FileSender', function() {
|
describe('FileSender', function() {
|
||||||
describe('upload', function() {
|
describe('upload', function() {
|
||||||
it('returns an OwnedFile on success', async function() {
|
it('returns an OwnedFile on success', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
assert.ok(file.id);
|
assert.ok(file.id);
|
||||||
assert.equal(file.name, blob.name);
|
assert.equal(file.name, archive.name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,9 +2,10 @@ const ece = require('http_ece');
|
||||||
require('buffer');
|
require('buffer');
|
||||||
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import Archive from '../../../app/archive';
|
||||||
import { b64ToArray } from '../../../app/utils';
|
import { b64ToArray } from '../../../app/utils';
|
||||||
import BlobSlicer from '../../../app/blobSlicer';
|
import { blobStream, concatStream } from '../../../app/streams';
|
||||||
import ECE from '../../../app/ece.js';
|
import { decryptStream, encryptStream } from '../../../app/ece.js';
|
||||||
|
|
||||||
const rs = 36;
|
const rs = 36;
|
||||||
|
|
||||||
|
@ -25,15 +26,52 @@ const encrypted = ece.encrypt(buffer, params);
|
||||||
const decrypted = ece.decrypt(encrypted, params);
|
const decrypted = ece.decrypt(encrypted, params);
|
||||||
|
|
||||||
describe('Streaming', function() {
|
describe('Streaming', function() {
|
||||||
|
describe('blobStream', function() {
|
||||||
|
it('reads the entire blob', async function() {
|
||||||
|
const len = 12345;
|
||||||
|
const chunkSize = 1024;
|
||||||
|
const blob = new Blob([new Uint8Array(len)]);
|
||||||
|
const stream = blobStream(blob, chunkSize);
|
||||||
|
const reader = stream.getReader();
|
||||||
|
let bytes = 0;
|
||||||
|
let data = await reader.read();
|
||||||
|
while (!data.done) {
|
||||||
|
bytes += data.value.byteLength;
|
||||||
|
assert.ok(data.value.byteLength <= chunkSize, 'chunk too big');
|
||||||
|
data = await reader.read();
|
||||||
|
}
|
||||||
|
assert.equal(bytes, len);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('concatStream', function() {
|
||||||
|
it('reads all the streams', async function() {
|
||||||
|
const count = 5;
|
||||||
|
const len = 12345;
|
||||||
|
const streams = Array.from({ length: count }, () =>
|
||||||
|
blobStream(new Blob([new Uint8Array(len)]))
|
||||||
|
);
|
||||||
|
const concat = concatStream(streams);
|
||||||
|
const reader = concat.getReader();
|
||||||
|
let bytes = 0;
|
||||||
|
let data = await reader.read();
|
||||||
|
while (!data.done) {
|
||||||
|
bytes += data.value.byteLength;
|
||||||
|
data = await reader.read();
|
||||||
|
}
|
||||||
|
assert.equal(bytes, len * count);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//testing against http_ece's implementation
|
//testing against http_ece's implementation
|
||||||
describe('ECE', function() {
|
describe('ECE', function() {
|
||||||
const key = b64ToArray(keystr);
|
const key = b64ToArray(keystr);
|
||||||
const salt = b64ToArray(testSalt).buffer;
|
const salt = b64ToArray(testSalt).buffer;
|
||||||
const blob = new Blob([str], { type: 'text/plain' });
|
|
||||||
|
|
||||||
it('can encrypt', async function() {
|
it('can encrypt', async function() {
|
||||||
const ece = new ECE(blob, key, 'encrypt', rs, salt);
|
const stream = new Archive([new Blob([str], { type: 'text/plain' })])
|
||||||
const encStream = await ece.transform();
|
.stream;
|
||||||
|
const encStream = encryptStream(stream, key, rs, salt);
|
||||||
const reader = encStream.getReader();
|
const reader = encStream.getReader();
|
||||||
|
|
||||||
let result = Buffer.from([]);
|
let result = Buffer.from([]);
|
||||||
|
@ -48,11 +86,8 @@ describe('Streaming', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can decrypt', async function() {
|
it('can decrypt', async function() {
|
||||||
const blobStream = new ReadableStream(
|
const stream = new Archive([new Blob([encrypted])]).stream;
|
||||||
new BlobSlicer(new Blob([encrypted]), rs)
|
const decStream = decryptStream(stream, key, rs);
|
||||||
);
|
|
||||||
const ece = new ECE(blobStream, key, 'decrypt', rs);
|
|
||||||
const decStream = await ece.transform();
|
|
||||||
|
|
||||||
const reader = decStream.getReader();
|
const reader = decStream.getReader();
|
||||||
let result = Buffer.from([]);
|
let result = Buffer.from([]);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import Archive from '../../../app/archive';
|
||||||
import FileSender from '../../../app/fileSender';
|
import FileSender from '../../../app/fileSender';
|
||||||
import FileReceiver from '../../../app/fileReceiver';
|
import FileReceiver from '../../../app/fileReceiver';
|
||||||
|
|
||||||
|
@ -11,12 +12,13 @@ const noSave = true || !headless; // only run the saveFile code if headless
|
||||||
// FileSender uses a File in real life but a Blob works for testing
|
// FileSender uses a File in real life but a Blob works for testing
|
||||||
const blob = new Blob([new ArrayBuffer(1024 * 128)], { type: 'text/plain' });
|
const blob = new Blob([new ArrayBuffer(1024 * 128)], { type: 'text/plain' });
|
||||||
blob.name = 'test.txt';
|
blob.name = 'test.txt';
|
||||||
|
const archive = new Archive([blob]);
|
||||||
navigator.serviceWorker.register('/serviceWorker.js');
|
navigator.serviceWorker.register('/serviceWorker.js');
|
||||||
|
|
||||||
describe('Upload / Download flow', function() {
|
describe('Upload / Download flow', function() {
|
||||||
this.timeout(0);
|
this.timeout(0);
|
||||||
it('can only download once by default', async function() {
|
it('can only download once by default', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
@ -36,7 +38,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('downloads with the correct password', async function() {
|
it('downloads with the correct password', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
await file.setPassword('magic');
|
await file.setPassword('magic');
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
|
@ -53,7 +55,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('blocks invalid passwords from downloading', async function() {
|
it('blocks invalid passwords from downloading', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
await file.setPassword('magic');
|
await file.setPassword('magic');
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
|
@ -81,7 +83,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('retries a bad nonce', async function() {
|
it('retries a bad nonce', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
@ -90,11 +92,11 @@ describe('Upload / Download flow', function() {
|
||||||
requiresPassword: false
|
requiresPassword: false
|
||||||
});
|
});
|
||||||
await fr.getMetadata();
|
await fr.getMetadata();
|
||||||
assert.equal(fr.fileInfo.name, blob.name);
|
assert.equal(fr.fileInfo.name, archive.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can cancel the upload', async function() {
|
it('can cancel the upload', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const up = fs.upload();
|
const up = fs.upload();
|
||||||
fs.cancel(); // before encrypting
|
fs.cancel(); // before encrypting
|
||||||
try {
|
try {
|
||||||
|
@ -122,7 +124,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can cancel the download', async function() {
|
it('can cancel the download', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
@ -142,7 +144,7 @@ describe('Upload / Download flow', function() {
|
||||||
|
|
||||||
it('can increase download count on download', async function() {
|
it('can increase download count on download', async function() {
|
||||||
this.timeout(0);
|
this.timeout(0);
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
@ -157,7 +159,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not increase download count when download cancelled', async function() {
|
it('does not increase download count when download cancelled', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
@ -178,7 +180,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can allow multiple downloads', async function() {
|
it('can allow multiple downloads', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
@ -204,7 +206,7 @@ describe('Upload / Download flow', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can delete the file before download', async function() {
|
it('can delete the file before download', async function() {
|
||||||
const fs = new FileSender(blob);
|
const fs = new FileSender(archive);
|
||||||
const file = await fs.upload();
|
const file = await fs.upload();
|
||||||
const fr = new FileReceiver({
|
const fr = new FileReceiver({
|
||||||
secretKey: file.toJSON().secretKey,
|
secretKey: file.toJSON().secretKey,
|
||||||
|
|
Loading…
Reference in New Issue