feat: Automatic plaintext backup option in Preferences > Backups will backup your notes and tags into plaintext, unencrypted folders on your computer. In addition, automatic encrypted text backups preference management has moved from the top-level menu in the desktop app to Preferences > Backups. (#2322)

This commit is contained in:
Mo
2023-05-02 11:05:10 -05:00
committed by GitHub
parent 3df23cdb5c
commit 7e3db49322
76 changed files with 1526 additions and 1013 deletions

View File

@@ -1,245 +0,0 @@
import { dialog, shell, WebContents } from 'electron'
import { promises as fs } from 'fs'
import path from 'path'
import { AppMessageType, MessageType } from '../../../../test/TestIpcMessage'
import { AppState } from '../../../AppState'
import { MessageToWebApp } from '../../Shared/IpcMessages'
import { StoreKeys } from '../Store/StoreKeys'
import { backups as str } from '../Strings'
import { Paths } from '../Types/Paths'
import {
deleteDir,
deleteDirContents,
ensureDirectoryExists,
FileDoesNotExist,
moveFiles,
openDirectoryPicker,
} from '../Utils/FileUtils'
import { handleTestMessage, send } from '../Utils/Testing'
import { isTesting, last } from '../Utils/Utils'
import { BackupsManagerInterface } from './BackupsManagerInterface'
function log(...message: any) {
console.log('BackupsManager:', ...message)
}
function logError(...message: any) {
console.error('BackupsManager:', ...message)
}
export const enum EnsureRecentBackupExists {
Success = 0,
BackupsAreDisabled = 1,
FailedToCreateBackup = 2,
}
export const BackupsDirectoryName = 'Standard Notes Backups'
const BackupFileExtension = '.txt'
function backupFileNameToDate(string: string): number {
string = path.basename(string, '.txt')
const dateTimeDelimiter = string.indexOf('T')
const date = string.slice(0, dateTimeDelimiter)
const time = string.slice(dateTimeDelimiter + 1).replace(/-/g, ':')
return Date.parse(date + 'T' + time)
}
function dateToSafeFilename(date: Date) {
return date.toISOString().replace(/:/g, '-')
}
async function copyDecryptScript(location: string) {
try {
await ensureDirectoryExists(location)
await fs.copyFile(Paths.decryptScript, path.join(location, path.basename(Paths.decryptScript)))
} catch (error) {
console.error(error)
}
}
export function createBackupsManager(webContents: WebContents, appState: AppState): BackupsManagerInterface {
let backupsLocation = appState.store.get(StoreKeys.BackupsLocation)
let backupsDisabled = appState.store.get(StoreKeys.BackupsDisabled)
let needsBackup = false
if (!backupsDisabled) {
void copyDecryptScript(backupsLocation)
}
determineLastBackupDate(backupsLocation)
.then((date) => appState.setBackupCreationDate(date))
.catch(console.error)
async function setBackupsLocation(location: string) {
const previousLocation = backupsLocation
if (previousLocation === location) {
return
}
const newLocation = path.join(location, BackupsDirectoryName)
let previousLocationFiles = await fs.readdir(previousLocation)
const backupFiles = previousLocationFiles
.filter((fileName) => fileName.endsWith(BackupFileExtension))
.map((fileName) => path.join(previousLocation, fileName))
await moveFiles(backupFiles, newLocation)
await copyDecryptScript(newLocation)
previousLocationFiles = await fs.readdir(previousLocation)
if (previousLocationFiles.length === 0 || previousLocationFiles[0] === path.basename(Paths.decryptScript)) {
await deleteDir(previousLocation)
}
/** Wait for the operation to be successful before saving new location */
backupsLocation = newLocation
appState.store.set(StoreKeys.BackupsLocation, backupsLocation)
}
async function saveBackupData(data: any) {
if (backupsDisabled) {
return
}
let success: boolean
let name: string | undefined
try {
name = await writeDataToFile(data)
log(`Data backup successfully saved: ${name}`)
success = true
appState.setBackupCreationDate(Date.now())
} catch (err) {
success = false
logError('An error occurred saving backup file', err)
}
webContents.send(MessageToWebApp.FinishedSavingBackup, { success })
if (isTesting()) {
send(AppMessageType.SavedBackup)
}
return name
}
function performBackup() {
if (backupsDisabled) {
return
}
webContents.send(MessageToWebApp.PerformAutomatedBackup)
}
async function writeDataToFile(data: any): Promise<string> {
await ensureDirectoryExists(backupsLocation)
const name = dateToSafeFilename(new Date()) + BackupFileExtension
const filePath = path.join(backupsLocation, name)
await fs.writeFile(filePath, data)
return name
}
let interval: NodeJS.Timeout | undefined
function beginBackups() {
if (interval) {
clearInterval(interval)
}
needsBackup = true
const hoursInterval = 12
const seconds = hoursInterval * 60 * 60
const milliseconds = seconds * 1000
interval = setInterval(performBackup, milliseconds)
}
function toggleBackupsStatus() {
backupsDisabled = !backupsDisabled
appState.store.set(StoreKeys.BackupsDisabled, backupsDisabled)
/** Create a backup on reactivation. */
if (!backupsDisabled) {
performBackup()
}
}
if (isTesting()) {
handleTestMessage(MessageType.DataArchive, (data: any) => saveBackupData(data))
handleTestMessage(MessageType.BackupsAreEnabled, () => !backupsDisabled)
handleTestMessage(MessageType.ToggleBackupsEnabled, toggleBackupsStatus)
handleTestMessage(MessageType.BackupsLocation, () => backupsLocation)
handleTestMessage(MessageType.PerformBackup, performBackup)
handleTestMessage(MessageType.CopyDecryptScript, copyDecryptScript)
handleTestMessage(MessageType.ChangeBackupsLocation, setBackupsLocation)
}
return {
get backupsAreEnabled() {
return !backupsDisabled
},
get backupsLocation() {
return backupsLocation
},
saveBackupData,
performBackup,
beginBackups,
toggleBackupsStatus,
async backupsCount(): Promise<number> {
let files = await fs.readdir(backupsLocation)
files = files.filter((fileName) => fileName.endsWith(BackupFileExtension))
return files.length
},
applicationDidBlur() {
if (needsBackup) {
needsBackup = false
performBackup()
}
},
viewBackups() {
void shell.openPath(backupsLocation)
},
async deleteBackups() {
await deleteDirContents(backupsLocation)
return copyDecryptScript(backupsLocation)
},
async changeBackupsLocation() {
const path = await openDirectoryPicker()
if (!path) {
return
}
try {
await setBackupsLocation(path)
performBackup()
} catch (e) {
logError(e)
void dialog.showMessageBox({
message: str().errorChangingDirectory(e),
})
}
},
}
}
async function determineLastBackupDate(backupsLocation: string): Promise<number | null> {
try {
const files = (await fs.readdir(backupsLocation))
.filter((filename) => filename.endsWith(BackupFileExtension) && !Number.isNaN(backupFileNameToDate(filename)))
.sort()
const lastBackupFileName = last(files)
if (!lastBackupFileName) {
return null
}
const backupDate = backupFileNameToDate(lastBackupFileName)
if (Number.isNaN(backupDate)) {
return null
}
return backupDate
} catch (error: any) {
if (error.code !== FileDoesNotExist) {
console.error(error)
}
return null
}
}

View File

@@ -1,13 +0,0 @@
export interface BackupsManagerInterface {
backupsAreEnabled: boolean
toggleBackupsStatus(): void
backupsLocation: string
backupsCount(): Promise<number>
applicationDidBlur(): void
changeBackupsLocation(): void
beginBackups(): void
performBackup(): void
deleteBackups(): Promise<void>
viewBackups(): void
saveBackupData(data: unknown): void
}

View File

@@ -1,18 +1,22 @@
import { LoggingDomain, log } from './../../../Logging'
import {
FileBackupRecord,
FileBackupsDevice,
FileBackupsMapping,
FileBackupReadToken,
FileBackupReadChunkResponse,
PlaintextBackupsMapping,
DesktopWatchedDirectoriesChange,
} from '@web/Application/Device/DesktopSnjsExports'
import { AppState } from 'app/AppState'
import { shell } from 'electron'
import { promises as fs } from 'fs'
import { WebContents, shell } from 'electron'
import { StoreKeys } from '../Store/StoreKeys'
import path from 'path'
import {
deleteFile,
deleteFileIfExists,
ensureDirectoryExists,
moveDirContents,
moveFile,
openDirectoryPicker,
readJSONFile,
writeFile,
@@ -20,6 +24,10 @@ import {
} from '../Utils/FileUtils'
import { FileDownloader } from './FileDownloader'
import { FileReadOperation } from './FileReadOperation'
import { Paths } from '../Types/Paths'
import { MessageToWebApp } from '../../Shared/IpcMessages'
const TextBackupFileExtension = '.txt'
export const FileBackupsConstantsV1 = {
Version: '1.0.0',
@@ -29,107 +37,112 @@ export const FileBackupsConstantsV1 = {
export class FilesBackupManager implements FileBackupsDevice {
private readOperations: Map<string, FileReadOperation> = new Map()
private plaintextMappingCache?: PlaintextBackupsMapping
constructor(private appState: AppState) {}
constructor(private appState: AppState, private webContents: WebContents) {}
public isFilesBackupsEnabled(): Promise<boolean> {
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsEnabled))
}
private async findUuidForPlaintextBackupFileName(
backupsDirectory: string,
targetFilename: string,
): Promise<string | undefined> {
const mapping = await this.getPlaintextBackupsMappingFile(backupsDirectory)
public async enableFilesBackups(): Promise<void> {
const currentLocation = await this.getFilesBackupsLocation()
if (!currentLocation) {
const result = await this.changeFilesBackupsLocation()
if (!result) {
return
const uuid = Object.keys(mapping.files).find((uuid) => {
const entries = mapping.files[uuid]
for (const entry of entries) {
const filePath = entry.path
const filename = path.basename(filePath)
if (filename === targetFilename) {
return true
}
}
}
return false
})
this.appState.store.set(StoreKeys.FileBackupsEnabled, true)
const mapping = this.getMappingFileFromDisk()
if (!mapping) {
await this.saveFilesBackupsMappingFile(this.defaultMappingFileValue())
}
return uuid
}
public disableFilesBackups(): Promise<void> {
this.appState.store.set(StoreKeys.FileBackupsEnabled, false)
public async migrateLegacyFileBackupsToNewStructure(newLocation: string): Promise<void> {
const legacyLocation = await this.getLegacyFilesBackupsLocation()
if (!legacyLocation) {
return
}
return Promise.resolve()
await ensureDirectoryExists(newLocation)
const legacyMappingLocation = `${legacyLocation}/info.json`
const newMappingLocation = this.getFileBackupsMappingFilePath(newLocation)
await ensureDirectoryExists(path.dirname(newMappingLocation))
await moveFile(legacyMappingLocation, newMappingLocation)
await moveDirContents(legacyLocation, newLocation)
}
public async changeFilesBackupsLocation(): Promise<string | undefined> {
const newPath = await openDirectoryPicker()
public async isLegacyFilesBackupsEnabled(): Promise<boolean> {
return this.appState.store.get(StoreKeys.LegacyFileBackupsEnabled)
}
if (!newPath) {
async wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
const value = this.appState.store.get(StoreKeys.LegacyTextBackupsDisabled)
return value === true
}
async getUserDocumentsDirectory(): Promise<string> {
return Paths.documentsDir
}
public async getLegacyFilesBackupsLocation(): Promise<string | undefined> {
return this.appState.store.get(StoreKeys.LegacyFileBackupsLocation)
}
async getLegacyTextBackupsLocation(): Promise<string | undefined> {
const savedLocation = this.appState.store.get(StoreKeys.LegacyTextBackupsLocation)
if (savedLocation) {
return savedLocation
}
const LegacyTextBackupsDirectory = 'Standard Notes Backups'
return `${Paths.homeDir}/${LegacyTextBackupsDirectory}`
}
public async presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string,
): Promise<string | undefined> {
const selectedDirectory = await openDirectoryPicker('Select')
if (!selectedDirectory) {
return undefined
}
const oldPath = await this.getFilesBackupsLocation()
const newPath = path.join(selectedDirectory, path.normalize(appendPath))
if (oldPath) {
await this.transferFilesBackupsToNewLocation(oldPath, newPath)
} else {
this.appState.store.set(StoreKeys.FileBackupsLocation, newPath)
await ensureDirectoryExists(newPath)
if (oldLocation) {
await moveDirContents(path.normalize(oldLocation), newPath)
}
return newPath
}
private async transferFilesBackupsToNewLocation(oldPath: string, newPath: string): Promise<void> {
const mapping = await this.getMappingFileFromDisk()
if (!mapping) {
return
}
const entries = Object.values(mapping.files)
for (const entry of entries) {
const sourcePath = path.join(oldPath, entry.relativePath)
const destinationPath = path.join(newPath, entry.relativePath)
await moveDirContents(sourcePath, destinationPath)
}
for (const entry of entries) {
entry.absolutePath = path.join(newPath, entry.relativePath)
}
const oldMappingFileLocation = this.getMappingFileLocation()
this.appState.store.set(StoreKeys.FileBackupsLocation, newPath)
const result = await this.saveFilesBackupsMappingFile(mapping)
if (result === 'success') {
await deleteFile(oldMappingFileLocation)
}
private getFileBackupsMappingFilePath(backupsLocation: string): string {
return `${backupsLocation}/.settings/info.json`
}
public getFilesBackupsLocation(): Promise<string> {
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsLocation))
private async getFileBackupsMappingFileFromDisk(backupsLocation: string): Promise<FileBackupsMapping | undefined> {
return readJSONFile<FileBackupsMapping>(this.getFileBackupsMappingFilePath(backupsLocation))
}
private getMappingFileLocation(): string {
const base = this.appState.store.get(StoreKeys.FileBackupsLocation)
return `${base}/info.json`
}
private async getMappingFileFromDisk(): Promise<FileBackupsMapping | undefined> {
return readJSONFile<FileBackupsMapping>(this.getMappingFileLocation())
}
private defaultMappingFileValue(): FileBackupsMapping {
private defaulFileBackupstMappingFileValue(): FileBackupsMapping {
return { version: FileBackupsConstantsV1.Version, files: {} }
}
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
const data = await this.getMappingFileFromDisk()
async getFilesBackupsMappingFile(backupsLocation: string): Promise<FileBackupsMapping> {
const data = await this.getFileBackupsMappingFileFromDisk(backupsLocation)
if (!data) {
return this.defaultMappingFileValue()
return this.defaulFileBackupstMappingFileValue()
}
for (const entry of Object.values(data.files)) {
@@ -139,23 +152,18 @@ export class FilesBackupManager implements FileBackupsDevice {
return data
}
async openFilesBackupsLocation(): Promise<void> {
const location = await this.getFilesBackupsLocation()
async openLocation(location: string): Promise<void> {
void shell.openPath(location)
}
async openFileBackup(record: FileBackupRecord): Promise<void> {
void shell.openPath(record.absolutePath)
}
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getMappingFileLocation(), file)
private async saveFilesBackupsMappingFile(location: string, file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getFileBackupsMappingFilePath(location), file)
return 'success'
}
async saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -164,9 +172,7 @@ export class FilesBackupManager implements FileBackupsDevice {
url: string
},
): Promise<'success' | 'failed'> {
const backupsDir = await this.getFilesBackupsLocation()
const fileDir = `${backupsDir}/${uuid}`
const fileDir = `${location}/${uuid}`
const metaFilePath = `${fileDir}/${FileBackupsConstantsV1.MetadataFileName}`
const binaryPath = `${fileDir}/${FileBackupsConstantsV1.BinaryFileName}`
@@ -184,25 +190,24 @@ export class FilesBackupManager implements FileBackupsDevice {
const result = await downloader.run()
if (result === 'success') {
const mapping = await this.getFilesBackupsMappingFile()
const mapping = await this.getFilesBackupsMappingFile(location)
mapping.files[uuid] = {
backedUpOn: new Date(),
absolutePath: fileDir,
relativePath: uuid,
metadataFileName: FileBackupsConstantsV1.MetadataFileName,
binaryFileName: FileBackupsConstantsV1.BinaryFileName,
version: FileBackupsConstantsV1.Version,
}
await this.saveFilesBackupsMappingFile(mapping)
await this.saveFilesBackupsMappingFile(location, mapping)
}
return result
}
async getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
const operation = new FileReadOperation(record)
async getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
const operation = new FileReadOperation(filePath)
this.readOperations.set(operation.token, operation)
@@ -224,4 +229,180 @@ export class FilesBackupManager implements FileBackupsDevice {
return result
}
async getTextBackupsCount(location: string): Promise<number> {
let files = await fs.readdir(location)
files = files.filter((fileName) => fileName.endsWith(TextBackupFileExtension))
return files.length
}
async saveTextBackupData(location: string, data: string): Promise<void> {
log(LoggingDomain.Backups, 'Saving text backup data', 'to', location)
let success: boolean
try {
await ensureDirectoryExists(location)
const name = `${new Date().toISOString().replace(/:/g, '-')}${TextBackupFileExtension}`
const filePath = path.join(location, name)
await fs.writeFile(filePath, data)
success = true
} catch (err) {
success = false
console.error('An error occurred saving backup file', err)
}
log(LoggingDomain.Backups, 'Finished saving text backup data', { success })
}
async copyDecryptScript(location: string) {
try {
await ensureDirectoryExists(location)
await fs.copyFile(Paths.decryptScript, path.join(location, path.basename(Paths.decryptScript)))
} catch (error) {
console.error(error)
}
}
private getPlaintextMappingFilePath(location: string): string {
return `${location}/.settings/info.json`
}
private async getPlaintextMappingFileFromDisk(location: string): Promise<PlaintextBackupsMapping | undefined> {
return readJSONFile<PlaintextBackupsMapping>(this.getPlaintextMappingFilePath(location))
}
private async savePlaintextBackupsMappingFile(
location: string,
file: PlaintextBackupsMapping,
): Promise<'success' | 'failed'> {
await writeJSONFile(this.getPlaintextMappingFilePath(location), file)
return 'success'
}
private defaultPlaintextMappingFileValue(): PlaintextBackupsMapping {
return { version: '1.0', files: {} }
}
async getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
if (this.plaintextMappingCache) {
return this.plaintextMappingCache
}
let data = await this.getPlaintextMappingFileFromDisk(location)
if (!data) {
data = this.defaultPlaintextMappingFileValue()
}
this.plaintextMappingCache = data
return data
}
async savePlaintextNoteBackup(
location: string,
uuid: string,
name: string,
tags: string[],
data: string,
): Promise<void> {
log(LoggingDomain.Backups, 'Saving plaintext note backup', uuid, 'to', location)
const mapping = await this.getPlaintextBackupsMappingFile(location)
if (!mapping.files[uuid]) {
mapping.files[uuid] = []
}
const removeNoteFromAllDirectories = async () => {
const records = mapping.files[uuid]
for (const record of records) {
const filePath = path.join(location, record.path)
await deleteFileIfExists(filePath)
}
mapping.files[uuid] = []
}
await removeNoteFromAllDirectories()
const writeFileToPath = async (absolutePath: string, filename: string, data: string, forTag?: string) => {
const findMappingRecord = (tag?: string) => {
const records = mapping.files[uuid]
return records.find((record) => record.tag === tag)
}
await ensureDirectoryExists(absolutePath)
const relativePath = forTag ?? ''
const filenameWithSlashesEscaped = filename.replace(/\//g, '\u2215')
const fileAbsolutePath = path.join(absolutePath, relativePath, filenameWithSlashesEscaped)
await writeFile(fileAbsolutePath, data)
const existingRecord = findMappingRecord(forTag)
if (!existingRecord) {
mapping.files[uuid].push({
tag: forTag,
path: path.join(relativePath, filename),
})
} else {
existingRecord.path = path.join(relativePath, filename)
existingRecord.tag = forTag
}
}
const uuidPart = uuid.split('-')[0]
const condensedUuidPart = uuidPart.substring(0, 4)
if (tags.length === 0) {
await writeFileToPath(location, `${name}-${condensedUuidPart}.txt`, data)
} else {
for (const tag of tags) {
await writeFileToPath(location, `${name}-${condensedUuidPart}.txt`, data, tag)
}
}
}
async persistPlaintextBackupsMappingFile(location: string): Promise<void> {
if (!this.plaintextMappingCache) {
return
}
await this.savePlaintextBackupsMappingFile(location, this.plaintextMappingCache)
}
async monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
const FEATURE_ENABLED = false
if (!FEATURE_ENABLED) {
return
}
try {
const watcher = fs.watch(backupsDirectory, { recursive: true })
for await (const event of watcher) {
const { eventType, filename } = event
if (eventType !== 'change' && eventType !== 'rename') {
continue
}
const itemUuid = await this.findUuidForPlaintextBackupFileName(backupsDirectory, filename)
if (itemUuid) {
try {
const change: DesktopWatchedDirectoriesChange = {
itemUuid,
path: path.join(backupsDirectory, filename),
type: eventType,
content: await fs.readFile(path.join(backupsDirectory, filename), 'utf-8'),
}
this.webContents.send(MessageToWebApp.WatchedDirectoriesChanges, [change])
} catch (err) {
log(LoggingDomain.Backups, 'Error processing watched change', err)
continue
}
}
}
} catch (err) {
if ((err as Error).name === 'AbortError') {
return
}
throw err
}
}
}

View File

@@ -1,6 +1,5 @@
import { FileBackupReadChunkResponse, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
import { FileBackupReadChunkResponse } from '@web/Application/Device/DesktopSnjsExports'
import fs from 'fs'
import path from 'path'
const ONE_MB = 1024 * 1024
const CHUNK_LIMIT = ONE_MB * 5
@@ -11,9 +10,9 @@ export class FileReadOperation {
private localFileId: number
private fileLength: number
constructor(backupRecord: FileBackupRecord) {
this.token = backupRecord.absolutePath
this.localFileId = fs.openSync(path.join(backupRecord.absolutePath, backupRecord.binaryFileName), 'r')
constructor(filePath: string) {
this.token = filePath
this.localFileId = fs.openSync(filePath, 'r')
this.fileLength = fs.fstatSync(this.localFileId).size
}

View File

@@ -20,7 +20,6 @@ import { checkForUpdate, openChangelog, showUpdateInstallationDialog } from '../
import { handleTestMessage } from '../Utils/Testing'
import { isDev, isTesting } from '../Utils/Utils'
import { MessageType } from './../../../../test/TestIpcMessage'
import { BackupsManagerInterface } from './../Backups/BackupsManagerInterface'
import { SpellcheckerManager } from './../SpellcheckerManager'
import { MenuManagerInterface } from './MenuManagerInterface'
@@ -112,14 +111,12 @@ function suggestionsMenu(
export function createMenuManager({
window,
appState,
backupsManager,
trayManager,
store,
spellcheckerManager,
}: {
window: Electron.BrowserWindow
appState: AppState
backupsManager: BackupsManagerInterface
trayManager: TrayManager
store: Store
spellcheckerManager?: SpellcheckerManager
@@ -167,7 +164,6 @@ export function createMenuManager({
editMenu(spellcheckerManager, reload),
viewMenu(window, store, reload),
windowMenu(store, trayManager, reload),
backupsMenu(backupsManager, reload),
updateMenu(window, appState),
...(isLinux() ? [keyringMenu(window, store)] : []),
helpMenu(window, shell),
@@ -468,34 +464,6 @@ function minimizeToTrayItem(store: Store, trayManager: TrayManager, reload: () =
}
}
function backupsMenu(archiveManager: BackupsManagerInterface, reload: () => any) {
return {
label: str().backups,
submenu: [
{
label: archiveManager.backupsAreEnabled ? str().disableAutomaticBackups : str().enableAutomaticBackups,
click() {
archiveManager.toggleBackupsStatus()
reload()
},
},
Separator,
{
label: str().changeBackupsLocation,
click() {
archiveManager.changeBackupsLocation()
},
},
{
label: str().openBackupsLocation,
click() {
void shell.openPath(archiveManager.backupsLocation)
},
},
],
}
}
function updateMenu(window: BrowserWindow, appState: AppState) {
const updateState = appState.updates
let label

View File

@@ -8,12 +8,11 @@ const rendererPath = path.join('file://', __dirname, '/renderer.js')
import {
FileBackupsDevice,
FileBackupsMapping,
FileBackupRecord,
FileBackupReadToken,
FileBackupReadChunkResponse,
PlaintextBackupsMapping,
} from '@web/Application/Device/DesktopSnjsExports'
import { app, BrowserWindow } from 'electron'
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
import { KeychainInterface } from '../Keychain/KeychainInterface'
import { MenuManagerInterface } from '../Menus/MenuManagerInterface'
import { Component, PackageManagerInterface } from '../Packages/PackageManagerInterface'
@@ -29,7 +28,6 @@ export class RemoteBridge implements CrossProcessBridge {
constructor(
private window: BrowserWindow,
private keychain: KeychainInterface,
private backups: BackupsManagerInterface,
private packages: PackageManagerInterface,
private search: SearchManagerInterface,
private data: RemoteDataInterface,
@@ -54,28 +52,30 @@ export class RemoteBridge implements CrossProcessBridge {
getKeychainValue: this.getKeychainValue.bind(this),
setKeychainValue: this.setKeychainValue.bind(this),
clearKeychainValue: this.clearKeychainValue.bind(this),
localBackupsCount: this.localBackupsCount.bind(this),
viewlocalBackups: this.viewlocalBackups.bind(this),
deleteLocalBackups: this.deleteLocalBackups.bind(this),
displayAppMenu: this.displayAppMenu.bind(this),
saveDataBackup: this.saveDataBackup.bind(this),
syncComponents: this.syncComponents.bind(this),
onMajorDataChange: this.onMajorDataChange.bind(this),
onSearch: this.onSearch.bind(this),
onInitialDataLoad: this.onInitialDataLoad.bind(this),
destroyAllData: this.destroyAllData.bind(this),
getFilesBackupsMappingFile: this.getFilesBackupsMappingFile.bind(this),
saveFilesBackupsFile: this.saveFilesBackupsFile.bind(this),
isFilesBackupsEnabled: this.isFilesBackupsEnabled.bind(this),
enableFilesBackups: this.enableFilesBackups.bind(this),
disableFilesBackups: this.disableFilesBackups.bind(this),
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
openFileBackup: this.openFileBackup.bind(this),
isLegacyFilesBackupsEnabled: this.isLegacyFilesBackupsEnabled.bind(this),
getLegacyFilesBackupsLocation: this.getLegacyFilesBackupsLocation.bind(this),
getFileBackupReadToken: this.getFileBackupReadToken.bind(this),
readNextChunk: this.readNextChunk.bind(this),
askForMediaAccess: this.askForMediaAccess.bind(this),
wasLegacyTextBackupsExplicitlyDisabled: this.wasLegacyTextBackupsExplicitlyDisabled.bind(this),
getLegacyTextBackupsLocation: this.getLegacyTextBackupsLocation.bind(this),
saveTextBackupData: this.saveTextBackupData.bind(this),
savePlaintextNoteBackup: this.savePlaintextNoteBackup.bind(this),
openLocation: this.openLocation.bind(this),
presentDirectoryPickerForLocationChangeAndTransferOld:
this.presentDirectoryPickerForLocationChangeAndTransferOld.bind(this),
getPlaintextBackupsMappingFile: this.getPlaintextBackupsMappingFile.bind(this),
persistPlaintextBackupsMappingFile: this.persistPlaintextBackupsMappingFile.bind(this),
getTextBackupsCount: this.getTextBackupsCount.bind(this),
migrateLegacyFileBackupsToNewStructure: this.migrateLegacyFileBackupsToNewStructure.bind(this),
getUserDocumentsDirectory: this.getUserDocumentsDirectory.bind(this),
monitorPlaintextBackupsLocationForChanges: this.monitorPlaintextBackupsLocationForChanges.bind(this),
}
}
@@ -135,51 +135,28 @@ export class RemoteBridge implements CrossProcessBridge {
return this.keychain.clearKeychainValue()
}
async localBackupsCount() {
return this.backups.backupsCount()
}
viewlocalBackups() {
this.backups.viewBackups()
}
async deleteLocalBackups() {
return this.backups.deleteBackups()
}
syncComponents(components: Component[]) {
void this.packages.syncComponents(components)
}
onMajorDataChange() {
this.backups.performBackup()
}
onSearch(text: string) {
this.search.findInPage(text)
}
onInitialDataLoad() {
this.backups.beginBackups()
}
destroyAllData() {
this.data.destroySensitiveDirectories()
}
saveDataBackup(data: unknown) {
this.backups.saveBackupData(data)
}
displayAppMenu() {
this.menus.popupMenu()
}
getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
return this.fileBackups.getFilesBackupsMappingFile()
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping> {
return this.fileBackups.getFilesBackupsMappingFile(location)
}
saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -188,43 +165,74 @@ export class RemoteBridge implements CrossProcessBridge {
url: string
},
): Promise<'success' | 'failed'> {
return this.fileBackups.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
return this.fileBackups.saveFilesBackupsFile(location, uuid, metaFile, downloadRequest)
}
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
return this.fileBackups.getFileBackupReadToken(record)
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
return this.fileBackups.getFileBackupReadToken(filePath)
}
readNextChunk(nextToken: string): Promise<FileBackupReadChunkResponse> {
return this.fileBackups.readNextChunk(nextToken)
}
public isFilesBackupsEnabled(): Promise<boolean> {
return this.fileBackups.isFilesBackupsEnabled()
public isLegacyFilesBackupsEnabled(): Promise<boolean> {
return this.fileBackups.isLegacyFilesBackupsEnabled()
}
public enableFilesBackups(): Promise<void> {
return this.fileBackups.enableFilesBackups()
public getLegacyFilesBackupsLocation(): Promise<string | undefined> {
return this.fileBackups.getLegacyFilesBackupsLocation()
}
public disableFilesBackups(): Promise<void> {
return this.fileBackups.disableFilesBackups()
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
return this.fileBackups.wasLegacyTextBackupsExplicitlyDisabled()
}
public changeFilesBackupsLocation(): Promise<string | undefined> {
return this.fileBackups.changeFilesBackupsLocation()
getLegacyTextBackupsLocation(): Promise<string | undefined> {
return this.fileBackups.getLegacyTextBackupsLocation()
}
public getFilesBackupsLocation(): Promise<string> {
return this.fileBackups.getFilesBackupsLocation()
saveTextBackupData(location: string, data: string): Promise<void> {
return this.fileBackups.saveTextBackupData(location, data)
}
public openFilesBackupsLocation(): Promise<void> {
return this.fileBackups.openFilesBackupsLocation()
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void> {
return this.fileBackups.savePlaintextNoteBackup(location, uuid, name, tags, data)
}
public openFileBackup(record: FileBackupRecord): Promise<void> {
return this.fileBackups.openFileBackup(record)
openLocation(path: string): Promise<void> {
return this.fileBackups.openLocation(path)
}
presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string | undefined,
): Promise<string | undefined> {
return this.fileBackups.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
}
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
return this.fileBackups.getPlaintextBackupsMappingFile(location)
}
persistPlaintextBackupsMappingFile(location: string): Promise<void> {
return this.fileBackups.persistPlaintextBackupsMappingFile(location)
}
getTextBackupsCount(location: string): Promise<number> {
return this.fileBackups.getTextBackupsCount(location)
}
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void> {
return this.fileBackups.migrateLegacyFileBackupsToNewStructure(newPath)
}
getUserDocumentsDirectory(): Promise<string> {
return this.fileBackups.getUserDocumentsDirectory()
}
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
return this.fileBackups.monitorPlaintextBackupsLocationForChanges(backupsDirectory)
}
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {

View File

@@ -4,30 +4,34 @@ export enum StoreKeys {
ExtServerHost = 'extServerHost',
UseSystemMenuBar = 'useSystemMenuBar',
MenuBarVisible = 'isMenuBarVisible',
BackupsLocation = 'backupsLocation',
BackupsDisabled = 'backupsDisabled',
MinimizeToTray = 'minimizeToTray',
EnableAutoUpdate = 'enableAutoUpdates',
ZoomFactor = 'zoomFactor',
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
UseNativeKeychain = 'useNativeKeychain',
FileBackupsEnabled = 'fileBackupsEnabled',
FileBackupsLocation = 'fileBackupsLocation',
LastRunVersion = 'LastRunVersion',
LegacyTextBackupsLocation = 'backupsLocation',
LegacyTextBackupsDisabled = 'backupsDisabled',
LegacyFileBackupsEnabled = 'fileBackupsEnabled',
LegacyFileBackupsLocation = 'fileBackupsLocation',
}
export interface StoreData {
[StoreKeys.ExtServerHost]: string
[StoreKeys.UseSystemMenuBar]: boolean
[StoreKeys.MenuBarVisible]: boolean
[StoreKeys.BackupsLocation]: string
[StoreKeys.BackupsDisabled]: boolean
[StoreKeys.MinimizeToTray]: boolean
[StoreKeys.EnableAutoUpdate]: boolean
[StoreKeys.UseNativeKeychain]: boolean | null
[StoreKeys.ZoomFactor]: number
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
[StoreKeys.FileBackupsEnabled]: boolean
[StoreKeys.FileBackupsLocation]: string
[StoreKeys.LastRunVersion]: string
[StoreKeys.LegacyTextBackupsLocation]: string
[StoreKeys.LegacyTextBackupsDisabled]: boolean
[StoreKeys.LegacyFileBackupsEnabled]: boolean
[StoreKeys.LegacyFileBackupsLocation]: string
}

View File

@@ -1,31 +1,30 @@
import fs from 'fs'
import path from 'path'
import { BackupsDirectoryName } from '../Backups/BackupsManager'
import { Language } from '../SpellcheckerManager'
import { FileDoesNotExist } from '../Utils/FileUtils'
import { ensureIsBoolean, isBoolean, isDev, isTesting } from '../Utils/Utils'
import { ensureIsBoolean, isBoolean } from '../Utils/Utils'
import { StoreData, StoreKeys } from './StoreKeys'
import { app, logError } from './Store'
import { logError } from './Store'
export function createSanitizedStoreData(data: any = {}): StoreData {
return {
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
[StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false),
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
? data[StoreKeys.UseNativeKeychain]
: null,
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
[StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]),
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
),
[StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false),
[StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation],
[StoreKeys.LastRunVersion]: data[StoreKeys.LastRunVersion],
[StoreKeys.LegacyTextBackupsLocation]: data[StoreKeys.LegacyTextBackupsLocation],
[StoreKeys.LegacyTextBackupsDisabled]: data[StoreKeys.LegacyTextBackupsDisabled],
[StoreKeys.LegacyFileBackupsEnabled]: data[StoreKeys.LegacyFileBackupsEnabled],
[StoreKeys.LegacyFileBackupsLocation]: data[StoreKeys.LegacyFileBackupsLocation],
}
}
function sanitizeZoomFactor(factor?: any): number {
@@ -35,29 +34,7 @@ function sanitizeZoomFactor(factor?: any): number {
return 1
}
}
function sanitizeBackupsLocation(location?: unknown): string {
const defaultPath = path.join(
isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'),
BackupsDirectoryName,
)
if (typeof location !== 'string') {
return defaultPath
}
try {
const stat = fs.lstatSync(location)
if (stat.isDirectory()) {
return location
}
/** Path points to something other than a directory */
return defaultPath
} catch (e) {
/** Path does not point to a valid directory */
logError(e)
return defaultPath
}
}
function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set<Language> | null {
if (!languages) {
return null

View File

@@ -13,8 +13,6 @@ export function createEnglishStrings(): Strings {
automaticUpdatesDisabled: 'Automatic Updates Disabled',
disableAutomaticBackups: 'Disable Automatic Backups',
enableAutomaticBackups: 'Enable Automatic Backups',
changeBackupsLocation: 'Change Backups Location',
openBackupsLocation: 'Open Backups Location',
emailSupport: 'Email Support',
website: 'Website',
gitHub: 'GitHub',
@@ -146,15 +144,5 @@ export function createEnglishStrings(): Strings {
},
unknownVersionName: 'Unknown',
},
backups: {
errorChangingDirectory(error: any): string {
return (
'An error occurred while changing your backups directory. ' +
'If this issue persists, please contact support with the following ' +
'information: \n' +
JSON.stringify(error)
)
},
},
}
}

View File

@@ -28,15 +28,5 @@ export function createFrenchStrings(): Strings {
},
extensions: fallback.extensions,
updates: fallback.updates,
backups: {
errorChangingDirectory(error: any): string {
return (
"Une erreur s'est produite lors du déplacement du dossier de " +
'sauvegardes. Si le problème est récurrent, contactez le support ' +
'technique (en anglais) avec les informations suivantes:\n' +
JSON.stringify(error)
)
},
},
}
}

View File

@@ -53,10 +53,6 @@ export function updates() {
return str().updates
}
export function backups() {
return str().backups
}
function stringsForLocale(locale: string): Strings {
if (locale === 'en' || locale.startsWith('en-')) {
return createEnglishStrings()

View File

@@ -4,7 +4,6 @@ export interface Strings {
tray: TrayStrings
extensions: ExtensionsStrings
updates: UpdateStrings
backups: BackupsStrings
}
interface AppMenuStrings {
@@ -18,8 +17,6 @@ interface AppMenuStrings {
automaticUpdatesDisabled: string
disableAutomaticBackups: string
enableAutomaticBackups: string
changeBackupsLocation: string
openBackupsLocation: string
emailSupport: string
website: string
gitHub: string
@@ -103,7 +100,3 @@ interface UpdateStrings {
}
unknownVersionName: string
}
interface BackupsStrings {
errorChangingDirectory(error: any): string
}

View File

@@ -36,6 +36,9 @@ export const Paths = {
get userDataDir(): string {
return app.getPath('userData')
},
get homeDir(): string {
return app.getPath('home')
},
get documentsDir(): string {
return app.getPath('documents')
},

View File

@@ -2,11 +2,10 @@ import { compareVersions } from 'compare-versions'
import { BrowserWindow, dialog, shell } from 'electron'
import electronLog from 'electron-log'
import { autoUpdater } from 'electron-updater'
import { action, autorun, computed, makeObservable, observable } from 'mobx'
import { action, computed, makeObservable, observable } from 'mobx'
import { MessageType } from '../../../test/TestIpcMessage'
import { AppState } from '../../AppState'
import { MessageToWebApp } from '../Shared/IpcMessages'
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
import { StoreKeys } from './Store/StoreKeys'
import { updates as str } from './Strings'
import { autoUpdatingAvailable } from './Types/Constants'
@@ -84,7 +83,7 @@ export class UpdateState {
let updatesSetup = false
export function setupUpdates(window: BrowserWindow, appState: AppState, backupsManager: BackupsManagerInterface): void {
export function setupUpdates(window: BrowserWindow, appState: AppState): void {
if (!autoUpdatingAvailable) {
return
}
@@ -97,22 +96,6 @@ export function setupUpdates(window: BrowserWindow, appState: AppState, backupsM
const updateState = appState.updates
function checkUpdateSafety(): boolean {
let canUpdate: boolean
if (appState.store.get(StoreKeys.BackupsDisabled)) {
canUpdate = true
} else {
canUpdate = updateState.enableAutoUpdate && isLessThanOneHourFromNow(appState.lastBackupDate)
}
autoUpdater.autoInstallOnAppQuit = canUpdate
autoUpdater.autoDownload = canUpdate
return canUpdate
}
autorun(checkUpdateSafety)
const oneHour = 1 * 60 * 60 * 1000
setInterval(checkUpdateSafety, oneHour)
autoUpdater.on('update-downloaded', (info: { version?: string }) => {
window.webContents.send(MessageToWebApp.UpdateAvailable, null)
updateState.autoUpdateHasBeenDownloaded(info.version || null)
@@ -122,10 +105,9 @@ export function setupUpdates(window: BrowserWindow, appState: AppState, backupsM
autoUpdater.on(MessageToWebApp.UpdateAvailable, (info: { version?: string }) => {
updateState.checkedForUpdate(info.version || null)
if (updateState.enableAutoUpdate) {
const canUpdate = checkUpdateSafety()
if (!canUpdate) {
backupsManager.performBackup()
}
const canUpdate = updateState.enableAutoUpdate
autoUpdater.autoInstallOnAppQuit = canUpdate
autoUpdater.autoDownload = canUpdate
}
})
autoUpdater.on('update-not-available', (info: { version?: string }) => {
@@ -164,46 +146,21 @@ function quitAndInstall(window: BrowserWindow) {
}, 0)
}
function isLessThanOneHourFromNow(date: number | null) {
const now = Date.now()
const onHourMs = 1 * 60 * 60 * 1000
return now - (date ?? 0) < onHourMs
}
export async function showUpdateInstallationDialog(parentWindow: BrowserWindow, appState: AppState): Promise<void> {
if (!appState.updates.latestVersion) {
return
}
if (appState.lastBackupDate && isLessThanOneHourFromNow(appState.lastBackupDate)) {
const result = await dialog.showMessageBox(parentWindow, {
type: 'info',
title: str().updateReady.title,
message: str().updateReady.message(appState.updates.latestVersion),
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
cancelId: 0,
})
const result = await dialog.showMessageBox(parentWindow, {
type: 'info',
title: str().updateReady.title,
message: str().updateReady.message(appState.updates.latestVersion),
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
cancelId: 0,
})
const buttonIndex = result.response
if (buttonIndex === 1) {
quitAndInstall(parentWindow)
}
} else {
const cancelId = 0
const result = await dialog.showMessageBox({
type: 'warning',
title: str().updateReady.title,
message: str().updateReady.noRecentBackupMessage,
detail: str().updateReady.noRecentBackupDetail(appState.lastBackupDate),
checkboxLabel: str().updateReady.noRecentBackupChecbox,
checkboxChecked: false,
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
cancelId,
})
if (!result.checkboxChecked || result.response === cancelId) {
return
}
const buttonIndex = result.response
if (buttonIndex === 1) {
quitAndInstall(parentWindow)
}
}

View File

@@ -27,9 +27,10 @@ export function debouncedJSONDiskWriter(durationMs: number, location: string, da
}, durationMs)
}
export async function openDirectoryPicker(): Promise<string | undefined> {
export async function openDirectoryPicker(buttonLabel?: string): Promise<string | undefined> {
const result = await dialog.showOpenDialog({
properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'],
buttonLabel: buttonLabel,
})
return result.filePaths[0]
@@ -63,6 +64,7 @@ export function writeJSONFileSync(filepath: string, data: unknown): void {
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8')
}
/** Creates the directory if it doesn't exist. */
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
try {
const stat = await fs.promises.lstat(dirPath)
@@ -251,7 +253,7 @@ export async function moveFiles(sources: string[], destDir: string): Promise<voi
return Promise.all(sources.map((fileName) => moveFile(fileName, path.join(destDir, path.basename(fileName)))))
}
async function moveFile(source: PathLike, destination: PathLike) {
export async function moveFile(source: PathLike, destination: PathLike) {
try {
await fs.promises.rename(source, destination)
} catch (_error) {
@@ -261,6 +263,14 @@ async function moveFile(source: PathLike, destination: PathLike) {
}
}
export async function deleteFileIfExists(filePath: PathLike): Promise<void> {
try {
await deleteFile(filePath)
} catch {
return
}
}
/** Deletes a file, handling EPERM and EBUSY errors on Windows. */
export async function deleteFile(filePath: PathLike): Promise<void> {
for (let i = 1, maxTries = 10; i < maxTries; i++) {

View File

@@ -6,8 +6,6 @@ import path from 'path'
import { AppMessageType, MessageType } from '../../../test/TestIpcMessage'
import { AppState } from '../../AppState'
import { MessageToWebApp } from '../Shared/IpcMessages'
import { createBackupsManager } from './Backups/BackupsManager'
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
import { FilesBackupManager } from './FileBackups/FileBackupsManager'
import { Keychain } from './Keychain/Keychain'
import { MediaManager } from './Media/MediaManager'
@@ -35,7 +33,6 @@ const WINDOW_MIN_HEIGHT = 400
export interface WindowState {
window: Electron.BrowserWindow
menuManager: MenuManagerInterface
backupsManager: BackupsManagerInterface
trayManager: TrayManager
}
@@ -64,7 +61,6 @@ export async function createWindowState({
;(global as any).RemoteBridge = new RemoteBridge(
window,
Keychain,
services.backupsManager,
services.packageManager,
services.searchManager,
{
@@ -93,7 +89,6 @@ export async function createWindowState({
window.on('blur', () => {
window.webContents.send(MessageToWebApp.WindowBlurred, null)
services.backupsManager.applicationDidBlur()
})
window.once('ready-to-show', () => {
@@ -201,8 +196,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
const searchManager = initializeSearchManager(window.webContents)
initializeZoomManager(window, appState.store)
const backupsManager = createBackupsManager(window.webContents, appState)
const updateManager = setupUpdates(window, appState, backupsManager)
const updateManager = setupUpdates(window, appState)
const trayManager = createTrayManager(window, appState.store)
const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale)
const mediaManager = new MediaManager()
@@ -214,16 +208,14 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
const menuManager = createMenuManager({
appState,
window,
backupsManager,
trayManager,
store: appState.store,
spellcheckerManager,
})
const fileBackupsManager = new FilesBackupManager(appState)
const fileBackupsManager = new FilesBackupManager(appState, window.webContents)
return {
backupsManager,
updateManager,
trayManager,
spellcheckerManager,

View File

@@ -3,52 +3,22 @@ import { Component } from '../Main/Packages/PackageManagerInterface'
export interface CrossProcessBridge extends FileBackupsDevice {
get extServerHost(): string
get useNativeKeychain(): boolean
get rendererPath(): string
get isMacOS(): boolean
get appVersion(): string
get useSystemMenuBar(): boolean
closeWindow(): void
minimizeWindow(): void
maximizeWindow(): void
unmaximizeWindow(): void
isWindowMaximized(): boolean
getKeychainValue(): Promise<unknown>
setKeychainValue: (value: unknown) => Promise<void>
clearKeychainValue(): Promise<boolean>
localBackupsCount(): Promise<number>
viewlocalBackups(): void
deleteLocalBackups(): Promise<void>
saveDataBackup(data: unknown): void
displayAppMenu(): void
syncComponents(components: Component[]): void
onMajorDataChange(): void
onSearch(text: string): void
onInitialDataLoad(): void
destroyAllData(): void
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
}

View File

@@ -1,11 +1,11 @@
import {
DesktopDeviceInterface,
Environment,
FileBackupsMapping,
RawKeychainValue,
FileBackupRecord,
FileBackupReadToken,
FileBackupReadChunkResponse,
FileBackupsMapping,
PlaintextBackupsMapping,
} from '@web/Application/Device/DesktopSnjsExports'
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
import { Component } from '../Main/Packages/PackageManagerInterface'
@@ -25,6 +25,33 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
super(appVersion)
}
openLocation(path: string): Promise<void> {
return this.remoteBridge.openLocation(path)
}
presentDirectoryPickerForLocationChangeAndTransferOld(
appendPath: string,
oldLocation?: string | undefined,
): Promise<string | undefined> {
return this.remoteBridge.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
}
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping> {
return this.remoteBridge.getFilesBackupsMappingFile(location)
}
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
return this.remoteBridge.getPlaintextBackupsMappingFile(location)
}
persistPlaintextBackupsMappingFile(location: string): Promise<void> {
return this.remoteBridge.persistPlaintextBackupsMappingFile(location)
}
getTextBackupsCount(location: string): Promise<number> {
return this.remoteBridge.getTextBackupsCount(location)
}
async getKeychainValue() {
if (this.useNativeKeychain) {
const keychainValue = await this.remoteBridge.getKeychainValue()
@@ -57,18 +84,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
this.remoteBridge.syncComponents(components)
}
onMajorDataChange() {
this.remoteBridge.onMajorDataChange()
}
onSearch(text: string) {
this.remoteBridge.onSearch(text)
}
onInitialDataLoad() {
this.remoteBridge.onInitialDataLoad()
}
async clearAllDataFromDevice(workspaceIdentifiers: string[]): Promise<{ killsApplication: boolean }> {
await super.clearAllDataFromDevice(workspaceIdentifiers)
@@ -77,69 +96,36 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
return { killsApplication: true }
}
async downloadBackup() {
const receiver = window.webClient
receiver.didBeginBackup()
try {
const data = await receiver.requestBackupFile()
if (data) {
this.remoteBridge.saveDataBackup(data)
} else {
receiver.didFinishBackup(false)
}
} catch (error) {
console.error(error)
receiver.didFinishBackup(false)
}
public isLegacyFilesBackupsEnabled(): Promise<boolean> {
return this.remoteBridge.isLegacyFilesBackupsEnabled()
}
async localBackupsCount() {
return this.remoteBridge.localBackupsCount()
public getLegacyFilesBackupsLocation(): Promise<string | undefined> {
return this.remoteBridge.getLegacyFilesBackupsLocation()
}
viewlocalBackups() {
this.remoteBridge.viewlocalBackups()
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
return this.remoteBridge.wasLegacyTextBackupsExplicitlyDisabled()
}
async deleteLocalBackups() {
return this.remoteBridge.deleteLocalBackups()
getUserDocumentsDirectory(): Promise<string> {
return this.remoteBridge.getUserDocumentsDirectory()
}
public isFilesBackupsEnabled(): Promise<boolean> {
return this.remoteBridge.isFilesBackupsEnabled()
getLegacyTextBackupsLocation(): Promise<string | undefined> {
return this.remoteBridge.getLegacyTextBackupsLocation()
}
public enableFilesBackups(): Promise<void> {
return this.remoteBridge.enableFilesBackups()
saveTextBackupData(workspaceId: string, data: string): Promise<void> {
return this.remoteBridge.saveTextBackupData(workspaceId, data)
}
public disableFilesBackups(): Promise<void> {
return this.remoteBridge.disableFilesBackups()
}
public changeFilesBackupsLocation(): Promise<string | undefined> {
return this.remoteBridge.changeFilesBackupsLocation()
}
public getFilesBackupsLocation(): Promise<string> {
return this.remoteBridge.getFilesBackupsLocation()
}
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
return this.remoteBridge.getFilesBackupsMappingFile()
}
async openFilesBackupsLocation(): Promise<void> {
return this.remoteBridge.openFilesBackupsLocation()
}
openFileBackup(record: FileBackupRecord): Promise<void> {
return this.remoteBridge.openFileBackup(record)
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void> {
return this.remoteBridge.savePlaintextNoteBackup(location, uuid, name, tags, data)
}
async saveFilesBackupsFile(
location: string,
uuid: string,
metaFile: string,
downloadRequest: {
@@ -148,17 +134,25 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
url: string
},
): Promise<'success' | 'failed'> {
return this.remoteBridge.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
return this.remoteBridge.saveFilesBackupsFile(location, uuid, metaFile, downloadRequest)
}
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
return this.remoteBridge.getFileBackupReadToken(record)
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
return this.remoteBridge.getFileBackupReadToken(filePath)
}
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void> {
return this.remoteBridge.migrateLegacyFileBackupsToNewStructure(newPath)
}
readNextChunk(token: string): Promise<FileBackupReadChunkResponse> {
return this.remoteBridge.readNextChunk(token)
}
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
return this.remoteBridge.monitorPlaintextBackupsLocationForChanges(backupsDirectory)
}
async performHardReset(): Promise<void> {
console.error('performHardReset is not yet implemented')
}

View File

@@ -1,28 +1,25 @@
import { IpcRendererEvent } from 'electron/renderer'
import { MessageToWebApp } from '../Shared/IpcMessages'
import { ElectronMainEvents, MainEventHandler } from '../Shared/ElectronMainEvents'
const { ipcRenderer } = require('electron')
const RemoteBridge = require('@electron/remote').getGlobal('RemoteBridge')
const { contextBridge } = require('electron')
type MainEventCallback = (event: IpcRendererEvent, value: any) => void
process.once('loaded', function () {
contextBridge.exposeInMainWorld('electronRemoteBridge', RemoteBridge.exposableValue)
contextBridge.exposeInMainWorld('electronMainEvents', {
handleUpdateAvailable: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.UpdateAvailable, callback),
const mainEvents: ElectronMainEvents = {
setUpdateAvailableHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.UpdateAvailable, handler),
handlePerformAutomatedBackup: (callback: MainEventCallback) =>
ipcRenderer.on(MessageToWebApp.PerformAutomatedBackup, callback),
setWindowBlurredHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.WindowBlurred, handler),
handleFinishedSavingBackup: (callback: MainEventCallback) =>
ipcRenderer.on(MessageToWebApp.FinishedSavingBackup, callback),
setWindowFocusedHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.WindowFocused, handler),
handleWindowBlurred: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.WindowBlurred, callback),
setWatchedDirectoriesChangeHandler: (handler: MainEventHandler) =>
ipcRenderer.on(MessageToWebApp.WatchedDirectoriesChanges, handler),
handleWindowFocused: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.WindowFocused, callback),
setInstallComponentCompleteHandler: (handler: MainEventHandler) =>
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, handler),
}
handleInstallComponentComplete: (callback: MainEventCallback) =>
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, callback),
})
contextBridge.exposeInMainWorld('electronMainEvents', mainEvents)
})

View File

@@ -1,8 +1,12 @@
import { DesktopClientRequiresWebMethods } from '@web/Application/Device/DesktopSnjsExports'
import {
DesktopClientRequiresWebMethods,
DesktopWatchedDirectoriesChanges,
} from '@web/Application/Device/DesktopSnjsExports'
import { StartApplication } from '@web/Application/Device/StartApplication'
import { IpcRendererEvent } from 'electron/renderer'
import { CrossProcessBridge } from './CrossProcessBridge'
import { DesktopDevice } from './DesktopDevice'
import { ElectronMainEvents } from '../Shared/ElectronMainEvents'
declare const DEFAULT_SYNC_SERVER: string
declare const WEBSOCKET_URL: string
@@ -23,7 +27,7 @@ declare global {
purchaseUrl: string
startApplication: StartApplication
zip: unknown
electronMainEvents: any
electronMainEvents: ElectronMainEvents
}
}
@@ -128,26 +132,22 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
}
}
window.electronMainEvents.handleUpdateAvailable(() => {
window.electronMainEvents.setUpdateAvailableHandler(() => {
window.webClient.updateAvailable()
})
window.electronMainEvents.handlePerformAutomatedBackup(() => {
void window.device.downloadBackup()
})
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: { success: boolean }) => {
window.webClient.didFinishBackup(data.success)
})
window.electronMainEvents.handleWindowBlurred(() => {
window.electronMainEvents.setWindowBlurredHandler(() => {
window.webClient.windowLostFocus()
})
window.electronMainEvents.handleWindowFocused(() => {
window.electronMainEvents.setWindowFocusedHandler(() => {
window.webClient.windowGainedFocus()
})
window.electronMainEvents.handleInstallComponentComplete((_: IpcRendererEvent, data: any) => {
window.electronMainEvents.setInstallComponentCompleteHandler((_: IpcRendererEvent, data: any) => {
void window.webClient.onComponentInstallationComplete(data.component, undefined)
})
window.electronMainEvents.setWatchedDirectoriesChangeHandler((_: IpcRendererEvent, changes: unknown) => {
void window.webClient.handleWatchedDirectoriesChanges(changes as DesktopWatchedDirectoriesChanges)
})

View File

@@ -0,0 +1,11 @@
import { IpcRendererEvent } from 'electron/renderer'
export type MainEventHandler = (event: IpcRendererEvent, value: unknown) => void
export interface ElectronMainEvents {
setUpdateAvailableHandler(handler: MainEventHandler): void
setWindowBlurredHandler(handler: MainEventHandler): void
setWindowFocusedHandler(handler: MainEventHandler): void
setInstallComponentCompleteHandler(handler: MainEventHandler): void
setWatchedDirectoriesChangeHandler(handler: MainEventHandler): void
}

View File

@@ -1,10 +1,9 @@
export enum MessageToWebApp {
UpdateAvailable = 'update-available',
PerformAutomatedBackup = 'download-backup',
FinishedSavingBackup = 'finished-saving-backup',
WindowBlurred = 'window-blurred',
WindowFocused = 'window-focused',
InstallComponentComplete = 'install-component-complete',
WatchedDirectoriesChanges = 'watched-directories-changes',
}
export enum MessageToMainProcess {