Skip to content

Commit

Permalink
Export: handle fs errors
Browse files Browse the repository at this point in the history
  • Loading branch information
charlag committed Dec 5, 2024
1 parent fe85040 commit 581f6e1
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 73 deletions.
2 changes: 1 addition & 1 deletion src/common/desktop/DesktopMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ async function createComponents(): Promise<Components> {
const dispatcher = new DesktopGlobalDispatcher(
desktopCommonSystemFacade,
new DesktopDesktopSystemFacade(wm, window, sock),
new DesktopExportFacade(tfs, electron, conf, window, dragIcons, mailboxExportPersistence, fs),
new DesktopExportFacade(tfs, electron, conf, window, dragIcons, mailboxExportPersistence, fs, dateProvider),
new DesktopExternalCalendarFacade(),
new DesktopFileFacade(window, conf, dateProvider, customFetch, electron, tfs, fs),
new DesktopInterWindowEventFacade(window, wm),
Expand Down
42 changes: 40 additions & 2 deletions src/common/desktop/export/DesktopExportFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { CancelledError } from "../../api/common/error/CancelledError.js"
import { ProgrammingError } from "../../api/common/error/ProgrammingError.js"
import { generateExportFileName, mailToEmlFile } from "../../../mail-app/mail/export/emlUtils.js"
import { MailboxExportPersistence, MailboxExportState } from "./MailboxExportPersistence.js"
import { DateProvider } from "../../api/common/DateProvider.js"
import { formatSortableDate } from "@tutao/tutanota-utils"
import { FileOpenError } from "../../api/common/error/FileOpenError.js"

const EXPORT_DIR = "export"

Expand All @@ -29,6 +32,7 @@ export class DesktopExportFacade implements ExportFacade {
private readonly dragIcons: Record<MailExportMode, NativeImage>,
private readonly mailboxExportPersistence: MailboxExportPersistence,
private readonly fs: typeof FsModule,
private readonly dateProvider: DateProvider,
) {}

async checkFileExistsInExportDir(fileName: string): Promise<boolean> {
Expand Down Expand Up @@ -92,17 +96,43 @@ export class DesktopExportFacade implements ExportFacade {
if (directory == null) {
throw new CancelledError("Directory picking canceled")
}
const folderName = `TutaExport-${formatSortableDate(new Date(this.dateProvider.now()))}`
const fullPath = await this.pickUniqueFileName(path.join(directory, folderName))
await this.fs.promises.mkdir(fullPath)
await this.mailboxExportPersistence.setStateForUser({
type: "running",
userId,
mailboxId,
exportDirectoryPath: directory,
exportDirectoryPath: fullPath,
mailBagId,
mailId,
exportedMails: 0,
})
}

private async pickUniqueFileName(path: string): Promise<string> {
let counter = 0
let currentCandidate = path
while (await this.fileExists(currentCandidate)) {
counter += 1
currentCandidate = path + `-${counter}`
}
return currentCandidate
}

private async fileExists(path: string): Promise<boolean> {
try {
await this.fs.promises.stat(path)
} catch (e) {
if (e.code === "ENOENT") {
return false
} else {
throw e
}
}
return true
}

async getMailboxExportState(userId: string): Promise<MailboxExportState | null> {
return await this.mailboxExportPersistence.getStateForUser(userId)
}
Expand All @@ -128,7 +158,15 @@ export class DesktopExportFacade implements ExportFacade {
const filename = generateExportFileName(bundle.subject, new Date(bundle.sentOn), "eml")
const fullPath = path.join(exportState.exportDirectoryPath, filename)
const file = mailToEmlFile(bundle, filename)
await this.fs.promises.writeFile(fullPath, file.data)
try {
await this.fs.promises.writeFile(fullPath, file.data)
} catch (e) {
if (e.code === "ENOENT" || e.code === "EPERM") {
throw new FileOpenError(`Could not write ${fullPath}`)
} else {
throw e
}
}
await this.mailboxExportPersistence.setStateForUser({
type: "running",
userId,
Expand Down
21 changes: 18 additions & 3 deletions src/mail-app/native/main/MailExportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { LoginController } from "../../../common/api/main/LoginController.js"
import { FileController } from "../../../common/file/FileController.js"
import { CancelledError } from "../../../common/api/common/error/CancelledError.js"
import { BulkMailLoader } from "../../workerUtils/index/BulkMailLoader.js"
import { FileOpenError } from "../../../common/api/common/error/FileOpenError.js"

export type MailExportState =
| { type: "idle" }
| { type: "exporting"; mailboxDetail: MailboxDetail; progress: number; exportedMails: number }
| { type: "error"; message: string }
| {
type: "finished"
mailboxDetail: MailboxDetail
Expand Down Expand Up @@ -63,6 +65,8 @@ export class MailExportController {
if (e instanceof CancelledError) {
console.log("Export start cancelled")
return
} else {
throw e
}
}

Expand Down Expand Up @@ -123,10 +127,12 @@ export class MailExportController {
private async runExport(mailboxDetail: MailboxDetail, mailBags: MailBag[], mailId: Id) {
for (const mailBag of mailBags) {
await this.exportMailBag(mailBag, mailId)
if (this._state().type !== "exporting") {
return
}
}

const currentState = this._state()
if (currentState.type != "exporting") {
if (this._state().type !== "exporting") {
return
}
await this.exportFacade.endMailboxExport(this.userId)
Expand Down Expand Up @@ -163,7 +169,16 @@ export class MailExportController {
if (this._state().type !== "exporting") {
return
}
await this.exportFacade.saveMailboxExport(mailBundle, this.userId, mailBag._id, getElementId(mail))
try {
await this.exportFacade.saveMailboxExport(mailBundle, this.userId, mailBag._id, getElementId(mail))
} catch (e) {
if (e instanceof FileOpenError) {
this._state({ type: "error", message: e.message })
return
} else {
throw e
}
}
}
currentStartId = getElementId(lastThrow(downloadedMails))
const currentState = this._state()
Expand Down
146 changes: 85 additions & 61 deletions src/mail-app/settings/MailExportSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,73 +52,97 @@ export class MailExportSettings implements Component<MailExportSettingsAttrs> {
dropdownWidth: 300,
disabled: state.type === "exporting",
} satisfies DropDownSelectorAttrs<MailboxDetail>),
state.type === "exporting"
? [
m(".flex-space-between.items-center.mt.mb-s", [
m(".flex-grow.mr", [
m(
"small.noselect",
lang.get("exportingEmails_label", {
"{count}": state.exportedMails,
}),
),
m(
".rel.full-width.mt-s",
{
style: {
"background-color": theme.content_border,
height: px(2),
},
},
m(ProgressBar, { progress: state.progress }),
),
]),
m(Button, {
label: "cancel_action",
type: ButtonType.Secondary,
click: () => {
vnode.attrs.mailExportController.cancelExport()
},
}),
]),
]
: state.type === "idle"
? [
m(".flex-space-between.items-center.mt.mb-s", [
this.renderState(vnode.attrs.mailExportController),
]
}

private renderState(controller: MailExportController): Children {
const state = controller.state()
switch (state.type) {
case "exporting":
return [
m(".flex-space-between.items-center.mt.mb-s", [
m(".flex-grow.mr", [
m(
"small.noselect",
vnode.attrs.mailExportController.lastExport
? lang.get("lastExportTime_Label", {
"{date}": formatDate(vnode.attrs.mailExportController.lastExport),
})
: null,
lang.get("exportingEmails_label", {
"{count}": state.exportedMails,
}),
),
m(Button, {
label: "export_action",
click: () => {
if (this.selectedMailbox) {
vnode.attrs.mailExportController.startExport(this.selectedMailbox)
}
m(
".rel.full-width.mt-s",
{
style: {
"background-color": theme.content_border,
height: px(2),
},
},
type: ButtonType.Secondary,
}),
]),
]
: [
m(".flex-space-between.items-center.mt.mb-s", [
m("small.noselect", lang.get("exportFinished_label")),
m(Button, {
label: "open_action",
click: () => this.onOpenClicked(vnode.attrs),
type: ButtonType.Secondary,
}),
m(ProgressBar, { progress: state.progress }),
),
]),
],
]
m(Button, {
label: "cancel_action",
type: ButtonType.Secondary,
click: () => {
controller.cancelExport()
},
}),
]),
]
case "idle":
return [
m(".flex-space-between.items-center.mt.mb-s", [
m(
"small.noselect",
controller.lastExport
? lang.get("lastExportTime_Label", {
"{date}": formatDate(controller.lastExport),
})
: null,
),
m(Button, {
label: "export_action",
click: () => {
if (this.selectedMailbox) {
controller.startExport(this.selectedMailbox)
}
},
type: ButtonType.Secondary,
}),
]),
]
case "error":
return [
m(".flex-space-between.items-center.mt.mb-s", [
m("small.noselect", state.message),
m(Button, {
label: "retry_action",
click: () => {
controller.cancelExport()
if (this.selectedMailbox) {
controller.startExport(this.selectedMailbox)
}
},
type: ButtonType.Secondary,
}),
]),
]
case "finished":
return [
m(".flex-space-between.items-center.mt.mb-s", [
m("small.noselect", lang.get("exportFinished_label")),
m(Button, {
label: "open_action",
click: () => this.onOpenClicked(controller),
type: ButtonType.Secondary,
}),
]),
]
}
}

private async onOpenClicked(attrs: MailExportSettingsAttrs) {
await attrs.mailExportController.openExportDirectory()
await attrs.mailExportController.cancelExport()
private async onOpenClicked(controller: MailExportController) {
await controller.openExportDirectory()
await controller.cancelExport()
}
}
Loading

0 comments on commit 581f6e1

Please sign in to comment.