Move away from streamsaver(for now)
This commit is contained in:
parent
a4da8f9e9e
commit
41bc2b6481
4 changed files with 51 additions and 344 deletions
|
@ -1,4 +1,3 @@
|
|||
import streamSaver from "streamsaver";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
|
@ -7,21 +6,18 @@ import { decryptFile } from "../DecryptFile";
|
|||
import { mediaFromContent } from "../../customisations/Media";
|
||||
import { formatFullDateNoDay } from "../../DateUtils";
|
||||
import { Direction, MatrixClient } from "matrix-js-sdk";
|
||||
import streamToZIP from "./ZipStream";
|
||||
import * as ponyfill from "web-streams-polyfill/ponyfill";
|
||||
import "web-streams-polyfill/ponyfill"; // to support streams API for older browsers
|
||||
import { MutableRefObject } from "react";
|
||||
import JSZip from "jszip";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
type FileStream = {
|
||||
type BlobFile = {
|
||||
name: string;
|
||||
stream(): ReadableStream;
|
||||
blob: Blob;
|
||||
};
|
||||
|
||||
export default abstract class Exporter {
|
||||
protected files: FileStream[];
|
||||
protected files: BlobFile[];
|
||||
protected client: MatrixClient;
|
||||
protected writer: WritableStreamDefaultWriter<any>;
|
||||
protected fileStream: WritableStream<any>;
|
||||
protected cancelled: boolean;
|
||||
|
||||
protected constructor(
|
||||
|
@ -34,7 +30,6 @@ export default abstract class Exporter {
|
|||
this.files = [];
|
||||
this.client = MatrixClientPeg.get();
|
||||
window.addEventListener("beforeunload", this.onBeforeUnload);
|
||||
window.addEventListener("onunload", this.abortWriter);
|
||||
}
|
||||
|
||||
protected onBeforeUnload(e: BeforeUnloadEvent): string {
|
||||
|
@ -50,7 +45,7 @@ export default abstract class Exporter {
|
|||
protected addFile(filePath: string, blob: Blob): void {
|
||||
const file = {
|
||||
name: filePath,
|
||||
stream: () => blob.stream(),
|
||||
blob,
|
||||
};
|
||||
this.files.push(file);
|
||||
}
|
||||
|
@ -58,67 +53,31 @@ export default abstract class Exporter {
|
|||
protected async downloadZIP(): Promise<any> {
|
||||
const filename = `matrix-export-${formatFullDateNoDay(new Date())}.zip`;
|
||||
|
||||
// Support for older browsers
|
||||
streamSaver.WritableStream = ponyfill.WritableStream;
|
||||
|
||||
const zip = new JSZip();
|
||||
// Create a writable stream to the directory
|
||||
this.fileStream = streamSaver.createWriteStream(filename);
|
||||
|
||||
if (!this.cancelled) this.updateProgress("Generating a ZIP");
|
||||
else return this.cleanUp();
|
||||
|
||||
this.writer = this.fileStream.getWriter();
|
||||
const files = this.files;
|
||||
for (const file of this.files) zip.file(file.name, file.blob);
|
||||
|
||||
const readableZipStream = streamToZIP({
|
||||
start(ctrl) {
|
||||
for (const file of files) ctrl.enqueue(file);
|
||||
ctrl.close();
|
||||
},
|
||||
});
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
|
||||
this.updateProgress("Writing to the file system");
|
||||
|
||||
const reader = readableZipStream.getReader();
|
||||
await this.pumpToFileStream(reader);
|
||||
await saveAs(content, filename);
|
||||
}
|
||||
|
||||
protected cleanUp(): string {
|
||||
console.log("Cleaning up...");
|
||||
window.removeEventListener("beforeunload", this.onBeforeUnload);
|
||||
window.removeEventListener("onunload", this.abortWriter);
|
||||
return "";
|
||||
}
|
||||
|
||||
public async cancelExport(): Promise<void> {
|
||||
console.log("Cancelling export...");
|
||||
this.cancelled = true;
|
||||
await this.abortWriter();
|
||||
}
|
||||
|
||||
protected async downloadPlainText(fileName: string, text: string): Promise<any> {
|
||||
this.fileStream = streamSaver.createWriteStream(fileName);
|
||||
this.writer = this.fileStream.getWriter();
|
||||
const data = new TextEncoder().encode(text);
|
||||
if (this.cancelled) return this.cleanUp();
|
||||
await this.writer.write(data);
|
||||
await this.writer.close();
|
||||
}
|
||||
|
||||
protected async abortWriter(): Promise<void> {
|
||||
await this.fileStream?.abort();
|
||||
await this.writer?.abort();
|
||||
}
|
||||
|
||||
protected async pumpToFileStream(reader: ReadableStreamDefaultReader): Promise<void> {
|
||||
const res = await reader.read();
|
||||
if (res.done) await this.writer.close();
|
||||
else {
|
||||
await this.writer.write(res.value);
|
||||
await this.pumpToFileStream(reader);
|
||||
}
|
||||
await saveAs(new Blob[text], fileName);
|
||||
}
|
||||
|
||||
protected setEventMetadata(event: MatrixEvent): MatrixEvent {
|
||||
|
|
|
@ -1,279 +0,0 @@
|
|||
// Based on https://github.com/jimmywarting/StreamSaver.js/blob/master/examples/zip-stream.js
|
||||
|
||||
/* global ReadableStream */
|
||||
|
||||
type TypedArray =
|
||||
| Int8Array
|
||||
| Uint8Array
|
||||
| Int16Array
|
||||
| Uint16Array
|
||||
| Int32Array
|
||||
| Uint32Array
|
||||
| Uint8ClampedArray
|
||||
| Float32Array
|
||||
| Float64Array;
|
||||
|
||||
/**
|
||||
* 32-bit cyclic redundancy check, or CRC-32 - checksum
|
||||
*/
|
||||
class Crc32 {
|
||||
crc: number;
|
||||
table: any;
|
||||
constructor() {
|
||||
this.crc = -1;
|
||||
this.table = (() => {
|
||||
let i;
|
||||
let j;
|
||||
let t;
|
||||
const table = [];
|
||||
|
||||
for (i = 0; i < 256; i++) {
|
||||
t = i;
|
||||
for (j = 0; j < 8; j++) {
|
||||
t = (t & 1)
|
||||
? (t >>> 1) ^ 0xEDB88320
|
||||
: t >>> 1;
|
||||
}
|
||||
table[i] = t;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
}
|
||||
|
||||
append(data: TypedArray) {
|
||||
let crc = this.crc | 0;
|
||||
const table = this.table;
|
||||
for (let offset = 0, len = data.length | 0; offset < len; offset++) {
|
||||
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
|
||||
}
|
||||
this.crc = crc;
|
||||
}
|
||||
|
||||
get() {
|
||||
return ~this.crc;
|
||||
}
|
||||
}
|
||||
|
||||
type DataHelper = {
|
||||
array: Uint8Array;
|
||||
view: DataView;
|
||||
};
|
||||
|
||||
const getDataHelper = (byteLength: number): DataHelper => {
|
||||
const uint8 = new Uint8Array(byteLength);
|
||||
return {
|
||||
array: uint8,
|
||||
view: new DataView(uint8.buffer),
|
||||
};
|
||||
};
|
||||
|
||||
type FileLike = File & {
|
||||
directory: string;
|
||||
comment: string;
|
||||
stream(): ReadableStream;
|
||||
};
|
||||
|
||||
type ZipObj = {
|
||||
crc?: Crc32;
|
||||
uncompressedLength: number;
|
||||
compressedLength: number;
|
||||
ctrl: ReadableStreamDefaultController;
|
||||
writeFooter: Function;
|
||||
writeHeader: Function;
|
||||
reader?: ReadableStreamDefaultReader;
|
||||
offset: number;
|
||||
header?: DataHelper;
|
||||
fileLike: FileLike;
|
||||
level: number;
|
||||
directory: boolean;
|
||||
};
|
||||
|
||||
const pump = (zipObj: ZipObj) => zipObj.reader ? zipObj.reader.read().then(chunk => {
|
||||
if (zipObj.crc) {
|
||||
if (chunk.done) return zipObj.writeFooter();
|
||||
const outputData = chunk.value;
|
||||
zipObj.crc.append(outputData);
|
||||
zipObj.uncompressedLength += outputData.length;
|
||||
zipObj.compressedLength += outputData.length;
|
||||
zipObj.ctrl.enqueue(outputData);
|
||||
} else {
|
||||
throw new Error('Missing zipObj.crc');
|
||||
}
|
||||
}) : undefined;
|
||||
|
||||
export default function streamToZIP(underlyingSource: UnderlyingSource) {
|
||||
const files = Object.create(null);
|
||||
const filenames: string[] = [];
|
||||
const encoder = new TextEncoder();
|
||||
let offset = 0;
|
||||
let activeZipIndex = 0;
|
||||
let ctrl: ReadableStreamDefaultController;
|
||||
let activeZipObject: ZipObj;
|
||||
let closed: boolean;
|
||||
|
||||
function next() {
|
||||
activeZipIndex++;
|
||||
activeZipObject = files[filenames[activeZipIndex]];
|
||||
if (activeZipObject) processNextChunk();
|
||||
else if (closed) closeZip();
|
||||
}
|
||||
|
||||
const zipWriter: ReadableStreamDefaultController = {
|
||||
desiredSize: null,
|
||||
|
||||
error(err) {
|
||||
console.error(err);
|
||||
},
|
||||
|
||||
enqueue(fileLike: FileLike) {
|
||||
if (closed) {
|
||||
throw new TypeError(
|
||||
"Cannot enqueue a chunk into a readable stream that is closed or has been requested to be closed",
|
||||
);
|
||||
}
|
||||
|
||||
let name = fileLike.name.trim();
|
||||
const date = new Date(typeof fileLike.lastModified === 'undefined' ? Date.now() : fileLike.lastModified);
|
||||
|
||||
if (fileLike.directory && !name.endsWith('/')) name += '/';
|
||||
// if file already exists, do not enqueue
|
||||
if (files[name]) return;
|
||||
|
||||
const nameBuf = encoder.encode(name);
|
||||
filenames.push(name);
|
||||
|
||||
const zipObject: ZipObj = files[name] = {
|
||||
level: 0,
|
||||
ctrl,
|
||||
directory: !!fileLike.directory,
|
||||
nameBuf,
|
||||
comment: encoder.encode(fileLike.comment || ''),
|
||||
compressedLength: 0,
|
||||
uncompressedLength: 0,
|
||||
offset,
|
||||
|
||||
writeHeader() {
|
||||
const header = getDataHelper(26);
|
||||
const data = getDataHelper(30 + nameBuf.length);
|
||||
|
||||
zipObject.offset = offset;
|
||||
zipObject.header = header;
|
||||
|
||||
if (zipObject.level !== 0 && !zipObject.directory) {
|
||||
header.view.setUint16(4, 0x0800);
|
||||
}
|
||||
|
||||
header.view.setUint32(0, 0x14000808);
|
||||
header.view.setUint16(
|
||||
6,
|
||||
(((date.getHours() << 6) | date.getMinutes()) << 5) | (date.getSeconds() / 2),
|
||||
true,
|
||||
);
|
||||
header.view.setUint16(
|
||||
8,
|
||||
((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) |
|
||||
date.getDate(),
|
||||
true,
|
||||
);
|
||||
header.view.setUint16(22, nameBuf.length, true);
|
||||
data.view.setUint32(0, 0x504b0304);
|
||||
data.array.set(header.array, 4);
|
||||
data.array.set(nameBuf, 30);
|
||||
offset += data.array.length;
|
||||
ctrl.enqueue(data.array);
|
||||
},
|
||||
|
||||
writeFooter() {
|
||||
const footer = getDataHelper(16);
|
||||
footer.view.setUint32(0, 0x504b0708);
|
||||
|
||||
if (zipObject.crc && zipObject.header) {
|
||||
zipObject.header.view.setUint32(10, zipObject.crc.get(), true);
|
||||
zipObject.header.view.setUint32(14, zipObject.compressedLength, true);
|
||||
zipObject.header.view.setUint32(18, zipObject.uncompressedLength, true);
|
||||
footer.view.setUint32(4, zipObject.crc.get(), true);
|
||||
footer.view.setUint32(8, zipObject.compressedLength, true);
|
||||
footer.view.setUint32(12, zipObject.uncompressedLength, true);
|
||||
}
|
||||
|
||||
ctrl.enqueue(footer.array);
|
||||
offset += zipObject.compressedLength + 16;
|
||||
next();
|
||||
},
|
||||
fileLike,
|
||||
};
|
||||
|
||||
if (!activeZipObject) {
|
||||
activeZipObject = zipObject;
|
||||
processNextChunk();
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
if (closed) {
|
||||
throw new TypeError(
|
||||
"Cannot close a readable stream that has already been requested to be closed",
|
||||
);
|
||||
}
|
||||
if (!activeZipObject) closeZip();
|
||||
closed = true;
|
||||
},
|
||||
};
|
||||
|
||||
function closeZip() {
|
||||
let length = 0;
|
||||
let index = 0;
|
||||
let indexFilename: number;
|
||||
let file: any;
|
||||
|
||||
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
|
||||
file = files[filenames[indexFilename]];
|
||||
length += 46 + file.nameBuf.length + file.comment.length;
|
||||
}
|
||||
const data = getDataHelper(length + 22);
|
||||
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
|
||||
file = files[filenames[indexFilename]];
|
||||
data.view.setUint32(index, 0x504b0102);
|
||||
data.view.setUint16(index + 4, 0x1400);
|
||||
data.array.set(file.header.array, index + 6);
|
||||
data.view.setUint16(index + 32, file.comment.length, true);
|
||||
if (file.directory) {
|
||||
data.view.setUint8(index + 38, 0x10);
|
||||
}
|
||||
data.view.setUint32(index + 42, file.offset, true);
|
||||
data.array.set(file.nameBuf, index + 46);
|
||||
data.array.set(file.comment, index + 46 + file.nameBuf.length);
|
||||
index += 46 + file.nameBuf.length + file.comment.length;
|
||||
}
|
||||
data.view.setUint32(index, 0x504b0506);
|
||||
data.view.setUint16(index + 8, filenames.length, true);
|
||||
data.view.setUint16(index + 10, filenames.length, true);
|
||||
data.view.setUint32(index + 12, length, true);
|
||||
data.view.setUint32(index + 16, offset, true);
|
||||
ctrl.enqueue(data.array);
|
||||
ctrl.close();
|
||||
}
|
||||
|
||||
function processNextChunk() {
|
||||
if (!activeZipObject) return;
|
||||
if (activeZipObject.reader) return pump(activeZipObject);
|
||||
if (activeZipObject.fileLike.stream) {
|
||||
activeZipObject.crc = new Crc32();
|
||||
activeZipObject.reader = activeZipObject.fileLike.stream().getReader();
|
||||
activeZipObject.writeHeader();
|
||||
} else next();
|
||||
}
|
||||
|
||||
return new ReadableStream({
|
||||
start: c => {
|
||||
ctrl = c;
|
||||
underlyingSource.start && Promise.resolve(underlyingSource.start(zipWriter));
|
||||
},
|
||||
pull() {
|
||||
return processNextChunk() || (
|
||||
underlyingSource.pull &&
|
||||
Promise.resolve(underlyingSource.pull(zipWriter))
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue