Handle export cancellation

pull/21833/head
Jaiwanth 2021-06-27 20:55:54 +05:30
parent 398d49245f
commit d46fe678b0
6 changed files with 107 additions and 54 deletions

View File

@ -7,13 +7,17 @@ import DialogButtons from "../elements/DialogButtons";
import Field from "../elements/Field"; import Field from "../elements/Field";
import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledRadioGroup from "../elements/StyledRadioGroup";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import exportConversationalHistory, { import {
exportFormats, exportFormats,
exportTypes, exportTypes,
textForFormat, textForFormat,
textForType, textForType,
} from "../../../utils/exportUtils/exportUtils"; } from "../../../utils/exportUtils/exportUtils";
import { IFieldState, IValidationResult } from "../elements/Validation"; import { IFieldState, IValidationResult } from "../elements/Validation";
import HTMLExporter from "../../../utils/exportUtils/HtmlExport";
import JSONExporter from "../../../utils/exportUtils/JSONExport";
import PlainTextExporter from "../../../utils/exportUtils/PlainTextExport";
import { useStateCallback } from "../../../hooks/useStateCallback";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
room: Room; room: Room;
@ -26,6 +30,52 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
const [numberOfMessages, setNumberOfMessages] = useState<number>(100); const [numberOfMessages, setNumberOfMessages] = useState<number>(100);
const [sizeLimit, setSizeLimit] = useState<number | null>(8); const [sizeLimit, setSizeLimit] = useState<number | null>(8);
const [sizeLimitRef, messageCountRef] = [useRef<any>(), useRef<any>()]; const [sizeLimitRef, messageCountRef] = [useRef<any>(), useRef<any>()];
const [Exporter, setExporter] = useStateCallback(
null,
async (Exporter: HTMLExporter | PlainTextExporter | JSONExporter) => {
await Exporter?.export();
},
);
const startExport = async () => {
const exportOptions = {
numberOfMessages,
attachmentsIncluded: includeAttachments,
maxSize: sizeLimit * 1024 * 1024,
};
switch (exportFormat) {
case exportFormats.HTML:
setExporter(
new HTMLExporter(
room,
exportTypes[exportType],
exportOptions,
),
);
break;
case exportFormats.JSON:
setExporter(
new JSONExporter(
room,
exportTypes[exportType],
exportOptions,
),
);
break;
case exportFormats.PLAIN_TEXT:
setExporter(
new PlainTextExporter(
room,
exportTypes[exportType],
exportOptions,
),
);
break;
default:
console.error("Unknown export format");
return;
}
};
const onExportClick = async () => { const onExportClick = async () => {
const isValidSize = await sizeLimitRef.current.validate({ const isValidSize = await sizeLimitRef.current.validate({
@ -43,16 +93,7 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
return; return;
} }
} }
await exportConversationalHistory( await startExport();
room,
exportFormats[exportFormat],
exportTypes[exportType],
{
numberOfMessages,
attachmentsIncluded: includeAttachments,
maxSize: sizeLimit * 1024 * 1024,
},
);
}; };
const onValidateSize = async ({ const onValidateSize = async ({
@ -118,7 +159,8 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
}; };
}; };
const onCancel = () => { const onCancel = async () => {
await Exporter?.cancelExport();
onFinished(false); onFinished(false);
}; };

View File

@ -21,16 +21,18 @@ export default abstract class Exporter {
protected client: MatrixClient; protected client: MatrixClient;
protected writer: WritableStreamDefaultWriter<any>; protected writer: WritableStreamDefaultWriter<any>;
protected fileStream: WritableStream<any>; protected fileStream: WritableStream<any>;
protected cancelled: boolean;
protected constructor( protected constructor(
protected room: Room, protected room: Room,
protected exportType: exportTypes, protected exportType: exportTypes,
protected exportOptions?: exportOptions, protected exportOptions?: exportOptions,
) { ) {
this.cancelled = false;
this.files = []; this.files = [];
this.client = MatrixClientPeg.get(); this.client = MatrixClientPeg.get();
window.addEventListener("beforeunload", this.onBeforeUnload); window.addEventListener("beforeunload", this.onBeforeUnload);
window.addEventListener("onunload", this.abortExport); window.addEventListener("onunload", this.abortWriter);
} }
protected onBeforeUnload(e: BeforeUnloadEvent) { protected onBeforeUnload(e: BeforeUnloadEvent) {
@ -55,7 +57,8 @@ export default abstract class Exporter {
// Create a writable stream to the directory // Create a writable stream to the directory
this.fileStream = streamSaver.createWriteStream(filename); this.fileStream = streamSaver.createWriteStream(filename);
console.info("Generating a ZIP..."); if (!this.cancelled) console.info("Generating a ZIP...");
else return this.cleanUp();
this.writer = this.fileStream.getWriter(); this.writer = this.fileStream.getWriter();
const files = this.files; const files = this.files;
@ -67,21 +70,37 @@ export default abstract class Exporter {
}, },
}); });
if (this.cancelled) return this.cleanUp();
console.info("Writing to the file system...") console.info("Writing to the file system...")
const reader = readableZipStream.getReader() const reader = readableZipStream.getReader()
await this.pumpToFileStream(reader); await this.pumpToFileStream(reader);
} }
protected cleanUp() {
console.log("Cleaning up...");
window.removeEventListener("beforeunload", this.onBeforeUnload);
window.removeEventListener("onunload", this.abortWriter);
return "";
}
public async cancelExport() {
console.log("Cancelling export...");
this.cancelled = true;
await this.abortWriter();
}
protected async downloadPlainText(fileName: string, text: string): Promise<any> { protected async downloadPlainText(fileName: string, text: string): Promise<any> {
this.fileStream = streamSaver.createWriteStream(fileName); this.fileStream = streamSaver.createWriteStream(fileName);
this.writer = this.fileStream.getWriter() this.writer = this.fileStream.getWriter()
const data = new TextEncoder().encode(text); const data = new TextEncoder().encode(text);
if (this.cancelled) return this.cleanUp();
await this.writer.write(data); await this.writer.write(data);
await this.writer.close(); await this.writer.close();
} }
protected async abortExport(): Promise<void> { protected async abortWriter(): Promise<void> {
await this.fileStream?.abort(); await this.fileStream?.abort();
await this.writer?.abort(); await this.writer?.abort();
} }
@ -134,6 +153,11 @@ export default abstract class Exporter {
const eventsPerCrawl = Math.min(limit, 1000); const eventsPerCrawl = Math.min(limit, 1000);
const res: any = await this.client.createMessagesRequest(this.room.roomId, prevToken, eventsPerCrawl, "b"); const res: any = await this.client.createMessagesRequest(this.room.roomId, prevToken, eventsPerCrawl, "b");
if (this.cancelled) {
this.cleanUp();
return [];
}
if (res.chunk.length === 0) break; if (res.chunk.length === 0) break;
limit -= res.chunk.length; limit -= res.chunk.length;

View File

@ -314,7 +314,7 @@ export default class HTMLExporter extends Exporter {
let prevEvent = null; let prevEvent = null;
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {
const event = events[i]; const event = events[i];
console.log("Processing event " + i + " out of " + events.length); if (this.cancelled) return this.cleanUp();
if (!haveTileForEvent(event)) continue; if (!haveTileForEvent(event)) continue;
content += this._wantsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : ""; content += this._wantsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : "";
@ -349,17 +349,18 @@ export default class HTMLExporter extends Exporter {
this.addFile(`icons/${iconName}`, new Blob([exportIcons[iconName]])); this.addFile(`icons/${iconName}`, new Blob([exportIcons[iconName]]));
} }
console.info("HTML creation successful!");
await this.downloadZIP(); await this.downloadZIP();
const exportEnd = performance.now(); const exportEnd = performance.now();
console.info("Export successful!") if (this.cancelled) {
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); console.info("Export cancelled successfully");
} else {
console.info("Export successful!")
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
}
window.removeEventListener("beforeunload", this.onBeforeUnload); this.cleanUp();
window.removeEventListener("onunload", this.abortExport);
} }
} }

View File

@ -64,6 +64,7 @@ ${json}
protected async createOutput(events: MatrixEvent[]) { protected async createOutput(events: MatrixEvent[]) {
let content = ""; let content = "";
for (const event of events) { for (const event of events) {
if (this.cancelled) return this.cleanUp();
if (!haveTileForEvent(event)) continue; if (!haveTileForEvent(event)) continue;
content += await this.getJSONString(event); content += await this.getJSONString(event);
} }
@ -93,11 +94,14 @@ ${json}
const exportEnd = performance.now(); const exportEnd = performance.now();
console.info("Export successful!") if (this.cancelled) {
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); console.info("Export cancelled successfully");
} else {
console.info("Export successful!")
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
}
window.removeEventListener("beforeunload", this.onBeforeUnload); this.cleanUp()
window.removeEventListener("onunload", this.abortExport);
} }
} }

View File

@ -81,6 +81,7 @@ export default class PlainTextExporter extends Exporter {
protected async createOutput(events: MatrixEvent[]) { protected async createOutput(events: MatrixEvent[]) {
let content = ""; let content = "";
for (const event of events) { for (const event of events) {
if (this.cancelled) return this.cleanUp();
if (!haveTileForEvent(event)) continue; if (!haveTileForEvent(event)) continue;
const textForEvent = await this._textForEvent(event); const textForEvent = await this._textForEvent(event);
content += textForEvent && `${new Date(event.getTs()).toLocaleString()} - ${textForEvent}\n`; content += textForEvent && `${new Date(event.getTs()).toLocaleString()} - ${textForEvent}\n`;
@ -101,6 +102,8 @@ export default class PlainTextExporter extends Exporter {
console.info("Creating output..."); console.info("Creating output...");
const text = await this.createOutput(res); const text = await this.createOutput(res);
if (this.cancelled) return this.cleanUp();
if (this.files.length) { if (this.files.length) {
this.addFile("export.txt", new Blob([text])); this.addFile("export.txt", new Blob([text]));
await this.downloadZIP(); await this.downloadZIP();
@ -111,11 +114,14 @@ export default class PlainTextExporter extends Exporter {
const exportEnd = performance.now(); const exportEnd = performance.now();
console.info("Export successful!") if (this.cancelled) {
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`); console.info("Export cancelled successfully");
} else {
console.info("Export successful!")
console.log(`Exported ${res.length} events in ${(exportEnd - fetchStart)/1000} seconds`);
}
window.removeEventListener("onunload", this.abortExport); this.cleanUp();
window.removeEventListener("beforeunload", this.onBeforeUnload);
} }
} }

View File

@ -1,8 +1,4 @@
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import HTMLExporter from "./HtmlExport";
import JSONExporter from "./JSONExport";
import PlainTextExporter from "./PlainTextExport";
export enum exportFormats { export enum exportFormats {
HTML = "HTML", HTML = "HTML",
@ -48,23 +44,3 @@ export interface exportOptions {
maxSize: number; maxSize: number;
} }
const exportConversationalHistory = async (
room: Room,
format: string,
exportType: exportTypes,
exportOptions?: exportOptions,
) => {
switch (format) {
case exportFormats.HTML:
await new HTMLExporter(room, exportType, exportOptions).export();
break;
case exportFormats.JSON:
await new JSONExporter(room, exportType, exportOptions).export();
break;
case exportFormats.PLAIN_TEXT:
await new PlainTextExporter(room, exportType, exportOptions).export();
break;
}
};
export default exportConversationalHistory;