From f68f310bd50a6048b171be0972bd0d9b473ba1fa Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Sun, 19 Apr 2026 01:48:55 +0100 Subject: [PATCH 01/10] Add the ability to delete the default profile --- packages/app-desktop/gui/ProfileEditor.tsx | 58 +++++++++++++++---- .../utils/deleteProfile.test.ts | 29 ---------- .../ProfileSwitcher/utils/deleteProfile.ts | 51 ++++++++++++---- packages/lib/services/profileConfig/index.ts | 4 -- 4 files changed, 89 insertions(+), 53 deletions(-) diff --git a/packages/app-desktop/gui/ProfileEditor.tsx b/packages/app-desktop/gui/ProfileEditor.tsx index c011bdfe9a8..863fa5516d2 100644 --- a/packages/app-desktop/gui/ProfileEditor.tsx +++ b/packages/app-desktop/gui/ProfileEditor.tsx @@ -7,7 +7,7 @@ import { themeStyle } from '@joplin/lib/theme'; import bridge from '../services/bridge'; import dialogs from './dialogs'; import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types'; -import { deleteProfileById, saveProfileConfig } from '@joplin/lib/services/profileConfig'; +import { deleteProfileById, isSubProfile, saveProfileConfig } from '@joplin/lib/services/profileConfig'; import Setting from '@joplin/lib/models/Setting'; import shim from '@joplin/lib/shim'; import Logger from '@joplin/utils/Logger'; @@ -150,18 +150,56 @@ const ProfileEditorComponent: React.FC = props => { }); if (!ok) return; + const subProfile = isSubProfile(profile); const rootDir = Setting.value('rootProfileDir'); - const profileDir = `${rootDir}/profile-${profile.id}`; - try { - await shim.fsDriver().remove(profileDir); - logger.info('Deleted profile directory: ', profileDir); - } catch (error) { - logger.error('Error deleting profile directory: ', error); - bridge().showErrorMessageBox(error.message); + // Deleting the default profile must be handled differently. We can't delete the whole directory because it contains other profiles and global settings + if (subProfile) { + const profileDir = `${rootDir}/profile-${profile.id}`; + + try { + await shim.fsDriver().remove(profileDir); + logger.info('Deleted profile directory: ', profileDir); + } catch (error) { + logger.error('Error deleting profile directory: ', error); + bridge().showErrorMessageBox(error.message); + } + + await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id)); + } else { + const dirsToDelete = ['cache', 'JoplinBackup', 'resources', 'tmp']; + const filesToDelete = ['database.sqlite', 'log.txt', 'settings.json']; + + // Delete directories + for (const dir of dirsToDelete) { + const fullPath = `${rootDir}/${dir}`; + try { + if (await shim.fsDriver().exists(fullPath)) { + await shim.fsDriver().remove(fullPath); + logger.info('Deleted directory: ', fullPath); + } + } catch (error) { + logger.error('Error deleting directory: ', fullPath, error); + bridge().showErrorMessageBox(error.message); + } + } + + // Delete files + for (const file of filesToDelete) { + const fullPath = `${rootDir}/${file}`; + try { + if (await shim.fsDriver().exists(fullPath)) { + await shim.fsDriver().unlink(fullPath); + logger.info('Deleted file: ', fullPath); + } + } catch (error) { + logger.error('Error deleting file: ', fullPath, error); + bridge().showErrorMessageBox(error.message); + } + } + + bridge().showErrorMessageBox(_('The default profile has been reset.')); } - - await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id)); }; return ( diff --git a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.test.ts b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.test.ts index 21770679b9b..fc7a21298e1 100644 --- a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.test.ts +++ b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.test.ts @@ -56,33 +56,4 @@ describe('deleteProfile', () => { expect(await pathExists(resourceDir)).toBe(false); expect(await pathExists(pluginDataDir)).toBe(false); }); - - it('should refuse to delete the default profile', async () => { - const config: ProfileConfig = { - version: CurrentProfileVersion, - currentProfileId: 'test', - profiles: [ - { - name: 'Testing', - id: DefaultProfileId, - }, - { - name: 'Another test', - id: 'test', - }, - ], - }; - - try { - await deleteProfile({ - profileConfig: config, - toDelete: config.profiles[0], - databaseDriver: new MockDatabaseDriver(), - }); - - expect('did not throw').toBe('threw'); - } catch (error) { - expect(String(error)).toMatch(/The default profile cannot be deleted/); - } - }); }); diff --git a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts index 546e581eba0..2744ed7735a 100644 --- a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts +++ b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts @@ -2,10 +2,11 @@ import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types import { getDatabaseName, getPluginDataDir, getResourceDir, saveProfileConfig } from '../../../services/profiles'; import { deleteProfileById, getCurrentProfile, isSubProfile } from '@joplin/lib/services/profileConfig'; import Setting from '@joplin/lib/models/Setting'; -import shim from '@joplin/lib/shim'; +import shim, { MessageBoxType } from '@joplin/lib/shim'; import Logger from '@joplin/utils/Logger'; import resolvePathWithinDir from '@joplin/lib/utils/resolvePathWithinDir'; import DatabaseDriver from '@joplin/lib/database-driver'; +import { _ } from '@joplin/lib/locale'; const logger = Logger.create('deleteProfile'); @@ -17,14 +18,17 @@ interface DeleteProfileOptions { const deleteProfile = async (options: DeleteProfileOptions) => { logger.info('Deleting profile config', options.toDelete.id); - // This step also verifies that the to-be-deleted profile is not the default profile, etc. - const newConfig = deleteProfileById(options.profileConfig, options.toDelete.id); - // Save the profile config early. If the later deletion steps fail, this prevents the user from - // opening a partially-deleted profile: - await saveProfileConfig(newConfig); - + if (options.toDelete.id === options.profileConfig.currentProfileId) throw new Error(_('The active profile cannot be deleted. Switch to a different profile and try again.')); const subProfile = isSubProfile(options.toDelete); - if (!subProfile) throw new Error('Deleting a sub-profile is not supported'); + + // Deleting the default profile must be handled differently. We can't delete the whole directory because it contains other profiles and global settings + if (subProfile) { + const newConfig = deleteProfileById(options.profileConfig, options.toDelete.id); + // Save the profile config early. If the later deletion steps fail, this prevents the user from + // opening a partially-deleted profile. The default profile does not get deleted from the list, + // but the data will be cleared + await saveProfileConfig(newConfig); + } // Retrieve and validate both the database name and resources directory // **before** doing any deletion. @@ -42,11 +46,38 @@ const deleteProfile = async (options: DeleteProfileOptions) => { logger.warn('Failed to delete database: ', error, '. Was the profile initialized?'); } - logger.info('Deleting resources directory', resourcesDir); - await shim.fsDriver().remove(resourcesDir); + if (subProfile) { + logger.info('Deleting resources directory', resourcesDir); + await shim.fsDriver().remove(resourcesDir); + } else { + try { + const items = await shim.fsDriver().readDirStats(resourcesDir); + + for (const item of items) { + const fileName = item.path; + if (item.isDirectory) continue; + + if (/^[a-z0-9]{32}\./.test(fileName)) { + const fullPath = `${resourcesDir}/${fileName}`; + try { + await shim.fsDriver().unlink(fullPath); + logger.info('Deleted resource file: ', fullPath); + } catch (error) { + logger.error('Error deleting resource file: ', fullPath, error); + } + } + } + } catch (error) { + logger.error('Error reading resources directory: ', resourcesDir, error); + } + } logger.info('Deleting plugin data directory', pluginDataDir); await shim.fsDriver().remove(pluginDataDir); + + if (!subProfile) { + await shim.showMessageBox(_('The default profile has been reset.'), { type: MessageBoxType.Info }); + } }; export default deleteProfile; diff --git a/packages/lib/services/profileConfig/index.ts b/packages/lib/services/profileConfig/index.ts index 33d172a8de2..8340129c44c 100644 --- a/packages/lib/services/profileConfig/index.ts +++ b/packages/lib/services/profileConfig/index.ts @@ -2,7 +2,6 @@ import { rtrimSlashes } from '../../path-utils'; import shim from '../../shim'; import { CurrentProfileVersion, defaultProfile, defaultProfileConfig, DefaultProfileId, Profile, ProfileConfig } from './types'; import { customAlphabet } from 'nanoid/non-secure'; -import { _ } from '../../locale'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export const migrateProfileConfig = (profileConfig: any, toVersion: number): ProfileConfig => { @@ -102,9 +101,6 @@ export const createNewProfile = (config: ProfileConfig, profileName: string) => }; export const deleteProfileById = (config: ProfileConfig, profileId: string): ProfileConfig => { - if (profileId === DefaultProfileId) throw new Error(_('The default profile cannot be deleted')); - if (profileId === config.currentProfileId) throw new Error(_('The active profile cannot be deleted. Switch to a different profile and try again.')); - const newProfiles = config.profiles.filter(p => p.id !== profileId); return { ...config, From cceaba56852c980c2523005a45baaaac57204198 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Sun, 19 Apr 2026 01:51:44 +0100 Subject: [PATCH 02/10] Tweak --- packages/app-desktop/gui/ProfileEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/ProfileEditor.tsx b/packages/app-desktop/gui/ProfileEditor.tsx index 863fa5516d2..0608a50dded 100644 --- a/packages/app-desktop/gui/ProfileEditor.tsx +++ b/packages/app-desktop/gui/ProfileEditor.tsx @@ -198,7 +198,7 @@ const ProfileEditorComponent: React.FC = props => { } } - bridge().showErrorMessageBox(_('The default profile has been reset.')); + bridge().showMessageBox(_('The default profile has been reset.')); } }; From 93bfdca291c0603b56c7602779eeff3463aa92c0 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Sun, 19 Apr 2026 02:32:26 +0100 Subject: [PATCH 03/10] Fix --- .../components/ProfileSwitcher/utils/deleteProfile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts index 2744ed7735a..d7cc6046cea 100644 --- a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts +++ b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts @@ -101,7 +101,7 @@ const getTargetResourceDirectory = ({ toDelete: target }: DeleteProfileOptions) // Add an extra check here to verify that deleting the other profile's resource directory // doesn't also delete **the active** profile's resource directory. On mobile, the resources // directory can sometimes contain other profile directories (e.g. in the case of the default profile). - if (resolvePathWithinDir(resourcesDir, Setting.value('resourceDir')) !== null) { + if (isSubProfile(target) && resolvePathWithinDir(resourcesDir, Setting.value('resourceDir')) !== null) { throw new Error('Refusing to delete a directory that contains the active profile\'s resource directory.'); } return resourcesDir; @@ -110,7 +110,7 @@ const getTargetResourceDirectory = ({ toDelete: target }: DeleteProfileOptions) const getTargetPluginDataDirectory = ({ toDelete: target }: DeleteProfileOptions) => { const pluginDataDir = getPluginDataDir(target, isSubProfile(target)); - if (resolvePathWithinDir(pluginDataDir, Setting.value('pluginDataDir')) !== null) { + if (isSubProfile(target) && resolvePathWithinDir(pluginDataDir, Setting.value('pluginDataDir')) !== null) { throw new Error('Refusing to delete a directory that contains the active profile\'s plugin data directory.'); } return pluginDataDir; From db80d150e708217221d5e3a1e16f417be127fa6f Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Sun, 19 Apr 2026 03:30:59 +0100 Subject: [PATCH 04/10] Fix issue --- .../components/ProfileSwitcher/utils/deleteProfile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts index d7cc6046cea..14b08774847 100644 --- a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts +++ b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts @@ -54,8 +54,8 @@ const deleteProfile = async (options: DeleteProfileOptions) => { const items = await shim.fsDriver().readDirStats(resourcesDir); for (const item of items) { + if (item.isDirectory()) continue; const fileName = item.path; - if (item.isDirectory) continue; if (/^[a-z0-9]{32}\./.test(fileName)) { const fullPath = `${resourcesDir}/${fileName}`; From d01e604b641889f2da70148000d49fb1d484a037 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 20 Apr 2026 00:33:49 +0100 Subject: [PATCH 05/10] Add deletion of keymap-desktop.json --- packages/app-desktop/gui/ProfileEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/ProfileEditor.tsx b/packages/app-desktop/gui/ProfileEditor.tsx index 0608a50dded..c315fb3d9c1 100644 --- a/packages/app-desktop/gui/ProfileEditor.tsx +++ b/packages/app-desktop/gui/ProfileEditor.tsx @@ -168,7 +168,7 @@ const ProfileEditorComponent: React.FC = props => { await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id)); } else { const dirsToDelete = ['cache', 'JoplinBackup', 'resources', 'tmp']; - const filesToDelete = ['database.sqlite', 'log.txt', 'settings.json']; + const filesToDelete = ['database.sqlite', 'log.txt', 'settings.json', 'keymap-desktop.json']; // Delete directories for (const dir of dirsToDelete) { From 29c0312d4e73420f95c03f39b272fe74b4aec012 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 20 Apr 2026 00:35:55 +0100 Subject: [PATCH 06/10] Remove error popups --- packages/app-desktop/gui/ProfileEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app-desktop/gui/ProfileEditor.tsx b/packages/app-desktop/gui/ProfileEditor.tsx index c315fb3d9c1..79c8804fa99 100644 --- a/packages/app-desktop/gui/ProfileEditor.tsx +++ b/packages/app-desktop/gui/ProfileEditor.tsx @@ -180,7 +180,6 @@ const ProfileEditorComponent: React.FC = props => { } } catch (error) { logger.error('Error deleting directory: ', fullPath, error); - bridge().showErrorMessageBox(error.message); } } @@ -194,7 +193,6 @@ const ProfileEditorComponent: React.FC = props => { } } catch (error) { logger.error('Error deleting file: ', fullPath, error); - bridge().showErrorMessageBox(error.message); } } From 38fa4a3432f79a04118f236a412d1d30a5405614 Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 20 Apr 2026 22:23:29 +0100 Subject: [PATCH 07/10] Retain global values in default profile settings.json --- packages/app-desktop/gui/ProfileEditor.tsx | 12 +++++++- packages/lib/models/Setting.ts | 29 ++++++++++++++----- packages/lib/models/settings/FileHandler.ts | 31 +++++++++++++++------ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/app-desktop/gui/ProfileEditor.tsx b/packages/app-desktop/gui/ProfileEditor.tsx index 79c8804fa99..e1659b5442c 100644 --- a/packages/app-desktop/gui/ProfileEditor.tsx +++ b/packages/app-desktop/gui/ProfileEditor.tsx @@ -168,7 +168,17 @@ const ProfileEditorComponent: React.FC = props => { await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id)); } else { const dirsToDelete = ['cache', 'JoplinBackup', 'resources', 'tmp']; - const filesToDelete = ['database.sqlite', 'log.txt', 'settings.json', 'keymap-desktop.json']; + const filesToDelete = ['database.sqlite', 'log.txt', 'keymap-desktop.json']; + + // Reset settings for the default profile, but retain global settings + try { + await Setting.resetDefaultProfileSettings(); + } catch (error) { + // If the first stage fails, nothing has happened, so throw an error. But if there is a failure in later steps, ignore errors but log them + logger.error('Error deleting the default profile: ', error); + bridge().showErrorMessageBox(error.message); + return; + } // Delete directories for (const dir of dirsToDelete) { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 83de77915df..dcf3d9322a3 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -1131,13 +1131,7 @@ class Setting extends BaseModel { return output; } - public static async saveAll() { - if (Setting.autoSaveEnabled && !this.saveTimeoutId_) return Promise.resolve(); - - logger.debug('Saving settings...'); - shim.clearTimeout(this.saveTimeoutId_); - this.saveTimeoutId_ = null; - + private static async getFileValuesAndDbUpdateQueries() { const keys = this.keys(); const valuesForFile: SettingValues = {}; @@ -1183,6 +1177,18 @@ class Setting extends BaseModel { } } + return { valuesForFile, queries }; + } + + public static async saveAll() { + if (Setting.autoSaveEnabled && !this.saveTimeoutId_) return Promise.resolve(); + + logger.debug('Saving settings...'); + shim.clearTimeout(this.saveTimeoutId_); + this.saveTimeoutId_ = null; + + const { valuesForFile, queries } = await Setting.getFileValuesAndDbUpdateQueries(); + await BaseModel.db().transactionExecBatch(queries); if (this.canUseFileStorage()) { @@ -1208,6 +1214,15 @@ class Setting extends BaseModel { logger.debug('Settings have been saved.'); } + public static async resetDefaultProfileSettings() { + const { valuesForFile } = await Setting.getFileValuesAndDbUpdateQueries(); + + if (this.canUseFileStorage()) { + const { globalSettings } = splitGlobalAndLocalSettings(valuesForFile); + await this.rootFileHandler.save(globalSettings, { overwrite: true }); + } + } + public static scheduleChangeEvent() { if (this.changeEventTimeoutId_) shim.clearTimeout(this.changeEventTimeoutId_); diff --git a/packages/lib/models/settings/FileHandler.ts b/packages/lib/models/settings/FileHandler.ts index bb12b2ee92b..d729c559a7c 100644 --- a/packages/lib/models/settings/FileHandler.ts +++ b/packages/lib/models/settings/FileHandler.ts @@ -7,6 +7,10 @@ const logger = Logger.create('Settings'); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export type SettingValues = Record; +export interface FileHandlerOptions { + overwrite?: boolean; +} + export default class FileHandler { private filePath_: string; @@ -49,15 +53,18 @@ export default class FileHandler { return result; } - public async save(values: SettingValues) { + public async save(values: SettingValues, options: FileHandlerOptions = {}) { values = { ...values }; - - // Merge with existing settings. This prevents settings stored by disabled or not-yet-loaded - // plugins from being deleted. - for (const key in this.parsedJsonCache_) { - const includesSetting = Object.prototype.hasOwnProperty.call(values, key); - if (!includesSetting) { - values[key] = this.parsedJsonCache_[key]; + const overwrite = !!options.overwrite; + + if (!overwrite) { + // Merge with existing settings. This prevents settings stored by disabled or not-yet-loaded + // plugins from being deleted. + for (const key in this.parsedJsonCache_) { + const includesSetting = Object.prototype.hasOwnProperty.call(values, key); + if (!includesSetting) { + values[key] = this.parsedJsonCache_[key]; + } } } @@ -70,6 +77,14 @@ export default class FileHandler { await shim.fsDriver().writeFile(this.filePath_, json, 'utf8'); this.valueJsonCache_ = json; + + if (overwrite) { + // Prevent pre-existing settings from being re-instated by subsequent saving of settings + this.parsedJsonCache_ = { + '$schema': Setting.schemaUrl, + ...values, + }; + } } } From a69b2dfba586dd9a91b962295a6de4ebe482887b Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Mon, 20 Apr 2026 22:39:54 +0100 Subject: [PATCH 08/10] PR feedback --- packages/lib/models/settings/FileHandler.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/lib/models/settings/FileHandler.ts b/packages/lib/models/settings/FileHandler.ts index d729c559a7c..dd1b5f29692 100644 --- a/packages/lib/models/settings/FileHandler.ts +++ b/packages/lib/models/settings/FileHandler.ts @@ -80,10 +80,7 @@ export default class FileHandler { if (overwrite) { // Prevent pre-existing settings from being re-instated by subsequent saving of settings - this.parsedJsonCache_ = { - '$schema': Setting.schemaUrl, - ...values, - }; + this.parsedJsonCache_ = values; } } From 7f314b3b7c4d44008651a8c6733f59461353642d Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Thu, 30 Apr 2026 12:47:19 +0100 Subject: [PATCH 09/10] Remove JoplinBackup from deleted directories --- packages/app-desktop/gui/ProfileEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/ProfileEditor.tsx b/packages/app-desktop/gui/ProfileEditor.tsx index e1659b5442c..c9714a75022 100644 --- a/packages/app-desktop/gui/ProfileEditor.tsx +++ b/packages/app-desktop/gui/ProfileEditor.tsx @@ -167,7 +167,7 @@ const ProfileEditorComponent: React.FC = props => { await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id)); } else { - const dirsToDelete = ['cache', 'JoplinBackup', 'resources', 'tmp']; + const dirsToDelete = ['cache', 'resources', 'tmp']; const filesToDelete = ['database.sqlite', 'log.txt', 'keymap-desktop.json']; // Reset settings for the default profile, but retain global settings From 0b69b841980605039202ed7d49f8d690a0184d0a Mon Sep 17 00:00:00 2001 From: mrjo118 Date: Fri, 1 May 2026 02:04:14 +0100 Subject: [PATCH 10/10] Optimise regex --- .../components/ProfileSwitcher/utils/deleteProfile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts index 14b08774847..b90be928c93 100644 --- a/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts +++ b/packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.ts @@ -57,7 +57,7 @@ const deleteProfile = async (options: DeleteProfileOptions) => { if (item.isDirectory()) continue; const fileName = item.path; - if (/^[a-z0-9]{32}\./.test(fileName)) { + if (/^[a-f0-9]{32}\./.test(fileName)) { const fullPath = `${resourcesDir}/${fileName}`; try { await shim.fsDriver().unlink(fullPath);