Handle export cancellation
parent
398d49245f
commit
d46fe678b0
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
Loading…
Reference in New Issue