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:
@@ -1,4 +1,4 @@
|
|||||||
PURCHASE_URL=https://website-dev.standardnotes.com/purchase
|
PURCHASE_URL=https://standardnotes.com/purchase
|
||||||
PLANS_URL=https://website-dev.standardnotes.com/plans
|
PLANS_URL=https://standardnotes.com/plans
|
||||||
DASHBOARD_URL=https://website-dev.standardnotes.com/dashboard
|
DASHBOARD_URL=https://standardnotes.com/dashboard
|
||||||
DEFAULT_SYNC_SERVER=https://api-dev.standardnotes.com
|
DEFAULT_SYNC_SERVER=https://api.standardnotes.com
|
||||||
@@ -1,51 +1,17 @@
|
|||||||
# Standard Notes
|
# Standard Notes Desktop App
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
[](https://twitter.com/standardnotes)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
This application makes use of the core JS/CSS/HTML code found in the [web repo](https://github.com/standardnotes/app). For issues related to the actual app experience, please post issues in the web repo.
|
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
Make sure [Yarn](https://classic.yarnpkg.com/en/) is installed on your system.
|
Most commands below hog up a terminal process and must be conducted in different tabs. Be sure to quit any production version of the app running on your system first.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:web # Or `yarn dev:web`
|
cd packages/snjs && yarn start # optional to watch snjs changes
|
||||||
yarn dev
|
cd packages/web && yarn watch # optional to watch web changes
|
||||||
|
yarn dev # to start compilation watch process for electron-related code
|
||||||
# In another terminal
|
yarn start # to start dev app
|
||||||
yarn start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
We use [commitlint](https://github.com/conventional-changelog/commitlint) to validate commit messages.
|
|
||||||
Before making a pull request, make sure to check the output of the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn lint
|
|
||||||
yarn test # Make sure to start `yarn dev` before running the tests, and quit any running Standard Notes applications so they don't conflict.
|
|
||||||
```
|
|
||||||
|
|
||||||
Pull requests should target the `develop` branch.
|
|
||||||
|
|
||||||
### Installing dependencies
|
|
||||||
|
|
||||||
To determine where to install a dependency:
|
|
||||||
|
|
||||||
- If it is only required for building, install it in `package.json`'s `devDependencies`
|
|
||||||
- If it is required at runtime but can be packaged by webpack, install it in `package.json`'s `dependencies`.
|
|
||||||
- If it must be distributed as a node module (not packaged by webpack), install it in `app/package.json`'s `dependencies`
|
|
||||||
- Also make sure to declare it as an external commonjs dependency in `webpack.common.js`.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Build for all platforms:
|
|
||||||
|
|
||||||
- `yarn release`
|
|
||||||
|
|
||||||
## Building natively on arm64
|
## Building natively on arm64
|
||||||
|
|
||||||
Building arm64 releases on amd64 systems is only possible with AppImage, Debian and universal "dir" targets.
|
Building arm64 releases on amd64 systems is only possible with AppImage, Debian and universal "dir" targets.
|
||||||
@@ -63,14 +29,6 @@ and making sure `$GEM_HOME/bin` is added to `$PATH`.
|
|||||||
|
|
||||||
Snap releases also require a working snapcraft / `snapd` installation.
|
Snap releases also require a working snapcraft / `snapd` installation.
|
||||||
|
|
||||||
Building can then be done by running:
|
|
||||||
|
|
||||||
- `yarn install`
|
|
||||||
|
|
||||||
Followed by
|
|
||||||
|
|
||||||
- `node scripts/build.mjs deb-arm64`
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
On Linux, download the latest AppImage from the [Releases](https://github.com/standardnotes/app/releases/latest) page, and give it executable permission:
|
On Linux, download the latest AppImage from the [Releases](https://github.com/standardnotes/app/releases/latest) page, and give it executable permission:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { action, makeObservable, observable } from 'mobx'
|
|
||||||
import { MessageType } from '../test/TestIpcMessage'
|
import { MessageType } from '../test/TestIpcMessage'
|
||||||
import { Store } from './javascripts/Main/Store/Store'
|
import { Store } from './javascripts/Main/Store/Store'
|
||||||
import { StoreKeys } from './javascripts/Main/Store/StoreKeys'
|
import { StoreKeys } from './javascripts/Main/Store/StoreKeys'
|
||||||
@@ -14,7 +13,6 @@ export class AppState {
|
|||||||
readonly startUrl = Urls.indexHtml
|
readonly startUrl = Urls.indexHtml
|
||||||
readonly isPrimaryInstance: boolean
|
readonly isPrimaryInstance: boolean
|
||||||
public willQuitApp = false
|
public willQuitApp = false
|
||||||
public lastBackupDate: number | null = null
|
|
||||||
public windowState?: WindowState
|
public windowState?: WindowState
|
||||||
public deepLinkUrl?: string
|
public deepLinkUrl?: string
|
||||||
public readonly updates: UpdateState
|
public readonly updates: UpdateState
|
||||||
@@ -28,11 +26,6 @@ export class AppState {
|
|||||||
this.lastRunVersion = this.store.get(StoreKeys.LastRunVersion) || 'unknown'
|
this.lastRunVersion = this.store.get(StoreKeys.LastRunVersion) || 'unknown'
|
||||||
this.store.set(StoreKeys.LastRunVersion, this.version)
|
this.store.set(StoreKeys.LastRunVersion, this.version)
|
||||||
|
|
||||||
makeObservable(this, {
|
|
||||||
lastBackupDate: observable,
|
|
||||||
setBackupCreationDate: action,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.updates = new UpdateState(this)
|
this.updates = new UpdateState(this)
|
||||||
|
|
||||||
if (isTesting()) {
|
if (isTesting()) {
|
||||||
@@ -45,8 +38,4 @@ export class AppState {
|
|||||||
public isRunningVersionForFirstTime(): boolean {
|
public isRunningVersionForFirstTime(): boolean {
|
||||||
return this.lastRunVersion !== this.version
|
return this.lastRunVersion !== this.version
|
||||||
}
|
}
|
||||||
|
|
||||||
setBackupCreationDate(date: number | null): void {
|
|
||||||
this.lastBackupDate = date
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
packages/desktop/app/Logging.ts
Normal file
21
packages/desktop/app/Logging.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { isDev } from './javascripts/Main/Utils/Utils'
|
||||||
|
import { log as utilsLog } from '@standardnotes/utils'
|
||||||
|
|
||||||
|
export const isDevMode = isDev()
|
||||||
|
|
||||||
|
export enum LoggingDomain {
|
||||||
|
Backups,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoggingStatus: Record<LoggingDomain, boolean> = {
|
||||||
|
[LoggingDomain.Backups]: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||||
|
if (!isDevMode || !LoggingStatus[domain]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utilsLog(LoggingDomain[domain], ...args)
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ export function initializeApplication(args: { app: Electron.App; ipcMain: Electr
|
|||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
/** Expose the app's state as a global variable. Useful for debugging */
|
/** Expose the app's state as a global variable. Useful for debugging */
|
||||||
;(global as any).appState = state
|
;(global as any).appState = state
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
state.windowState?.window.webContents.openDevTools()
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ function migrateSnapStorage() {
|
|||||||
error?.message ?? error,
|
error?.message ?? error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
store.set(StoreKeys.BackupsLocation, newLocation)
|
store.set(StoreKeys.LegacyTextBackupsLocation, newLocation)
|
||||||
console.log('Migration: finished moving backups directory.')
|
console.log('Migration: finished moving backups directory.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import { LoggingDomain, log } from './../../../Logging'
|
||||||
import {
|
import {
|
||||||
FileBackupRecord,
|
|
||||||
FileBackupsDevice,
|
FileBackupsDevice,
|
||||||
FileBackupsMapping,
|
FileBackupsMapping,
|
||||||
FileBackupReadToken,
|
FileBackupReadToken,
|
||||||
FileBackupReadChunkResponse,
|
FileBackupReadChunkResponse,
|
||||||
|
PlaintextBackupsMapping,
|
||||||
|
DesktopWatchedDirectoriesChange,
|
||||||
} from '@web/Application/Device/DesktopSnjsExports'
|
} from '@web/Application/Device/DesktopSnjsExports'
|
||||||
import { AppState } from 'app/AppState'
|
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 { StoreKeys } from '../Store/StoreKeys'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import {
|
import {
|
||||||
deleteFile,
|
deleteFileIfExists,
|
||||||
ensureDirectoryExists,
|
ensureDirectoryExists,
|
||||||
moveDirContents,
|
moveDirContents,
|
||||||
|
moveFile,
|
||||||
openDirectoryPicker,
|
openDirectoryPicker,
|
||||||
readJSONFile,
|
readJSONFile,
|
||||||
writeFile,
|
writeFile,
|
||||||
@@ -20,6 +24,10 @@ import {
|
|||||||
} from '../Utils/FileUtils'
|
} from '../Utils/FileUtils'
|
||||||
import { FileDownloader } from './FileDownloader'
|
import { FileDownloader } from './FileDownloader'
|
||||||
import { FileReadOperation } from './FileReadOperation'
|
import { FileReadOperation } from './FileReadOperation'
|
||||||
|
import { Paths } from '../Types/Paths'
|
||||||
|
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||||
|
|
||||||
|
const TextBackupFileExtension = '.txt'
|
||||||
|
|
||||||
export const FileBackupsConstantsV1 = {
|
export const FileBackupsConstantsV1 = {
|
||||||
Version: '1.0.0',
|
Version: '1.0.0',
|
||||||
@@ -29,107 +37,112 @@ export const FileBackupsConstantsV1 = {
|
|||||||
|
|
||||||
export class FilesBackupManager implements FileBackupsDevice {
|
export class FilesBackupManager implements FileBackupsDevice {
|
||||||
private readOperations: Map<string, FileReadOperation> = new Map()
|
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> {
|
private async findUuidForPlaintextBackupFileName(
|
||||||
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsEnabled))
|
backupsDirectory: string,
|
||||||
}
|
targetFilename: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const mapping = await this.getPlaintextBackupsMappingFile(backupsDirectory)
|
||||||
|
|
||||||
public async enableFilesBackups(): Promise<void> {
|
const uuid = Object.keys(mapping.files).find((uuid) => {
|
||||||
const currentLocation = await this.getFilesBackupsLocation()
|
const entries = mapping.files[uuid]
|
||||||
|
for (const entry of entries) {
|
||||||
if (!currentLocation) {
|
const filePath = entry.path
|
||||||
const result = await this.changeFilesBackupsLocation()
|
const filename = path.basename(filePath)
|
||||||
|
if (filename === targetFilename) {
|
||||||
if (!result) {
|
return true
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
this.appState.store.set(StoreKeys.FileBackupsEnabled, true)
|
return uuid
|
||||||
|
|
||||||
const mapping = this.getMappingFileFromDisk()
|
|
||||||
|
|
||||||
if (!mapping) {
|
|
||||||
await this.saveFilesBackupsMappingFile(this.defaultMappingFileValue())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public disableFilesBackups(): Promise<void> {
|
public async migrateLegacyFileBackupsToNewStructure(newLocation: string): Promise<void> {
|
||||||
this.appState.store.set(StoreKeys.FileBackupsEnabled, false)
|
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> {
|
public async isLegacyFilesBackupsEnabled(): Promise<boolean> {
|
||||||
const newPath = await openDirectoryPicker()
|
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
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldPath = await this.getFilesBackupsLocation()
|
const newPath = path.join(selectedDirectory, path.normalize(appendPath))
|
||||||
|
|
||||||
if (oldPath) {
|
await ensureDirectoryExists(newPath)
|
||||||
await this.transferFilesBackupsToNewLocation(oldPath, newPath)
|
|
||||||
} else {
|
if (oldLocation) {
|
||||||
this.appState.store.set(StoreKeys.FileBackupsLocation, newPath)
|
await moveDirContents(path.normalize(oldLocation), newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPath
|
return newPath
|
||||||
}
|
}
|
||||||
|
|
||||||
private async transferFilesBackupsToNewLocation(oldPath: string, newPath: string): Promise<void> {
|
private getFileBackupsMappingFilePath(backupsLocation: string): string {
|
||||||
const mapping = await this.getMappingFileFromDisk()
|
return `${backupsLocation}/.settings/info.json`
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFilesBackupsLocation(): Promise<string> {
|
private async getFileBackupsMappingFileFromDisk(backupsLocation: string): Promise<FileBackupsMapping | undefined> {
|
||||||
return Promise.resolve(this.appState.store.get(StoreKeys.FileBackupsLocation))
|
return readJSONFile<FileBackupsMapping>(this.getFileBackupsMappingFilePath(backupsLocation))
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMappingFileLocation(): string {
|
private defaulFileBackupstMappingFileValue(): FileBackupsMapping {
|
||||||
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 {
|
|
||||||
return { version: FileBackupsConstantsV1.Version, files: {} }
|
return { version: FileBackupsConstantsV1.Version, files: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
|
async getFilesBackupsMappingFile(backupsLocation: string): Promise<FileBackupsMapping> {
|
||||||
const data = await this.getMappingFileFromDisk()
|
const data = await this.getFileBackupsMappingFileFromDisk(backupsLocation)
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return this.defaultMappingFileValue()
|
return this.defaulFileBackupstMappingFileValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of Object.values(data.files)) {
|
for (const entry of Object.values(data.files)) {
|
||||||
@@ -139,23 +152,18 @@ export class FilesBackupManager implements FileBackupsDevice {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFilesBackupsLocation(): Promise<void> {
|
async openLocation(location: string): Promise<void> {
|
||||||
const location = await this.getFilesBackupsLocation()
|
|
||||||
|
|
||||||
void shell.openPath(location)
|
void shell.openPath(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFileBackup(record: FileBackupRecord): Promise<void> {
|
private async saveFilesBackupsMappingFile(location: string, file: FileBackupsMapping): Promise<'success' | 'failed'> {
|
||||||
void shell.openPath(record.absolutePath)
|
await writeJSONFile(this.getFileBackupsMappingFilePath(location), file)
|
||||||
}
|
|
||||||
|
|
||||||
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
|
|
||||||
await writeJSONFile(this.getMappingFileLocation(), file)
|
|
||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveFilesBackupsFile(
|
async saveFilesBackupsFile(
|
||||||
|
location: string,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
metaFile: string,
|
metaFile: string,
|
||||||
downloadRequest: {
|
downloadRequest: {
|
||||||
@@ -164,9 +172,7 @@ export class FilesBackupManager implements FileBackupsDevice {
|
|||||||
url: string
|
url: string
|
||||||
},
|
},
|
||||||
): Promise<'success' | 'failed'> {
|
): Promise<'success' | 'failed'> {
|
||||||
const backupsDir = await this.getFilesBackupsLocation()
|
const fileDir = `${location}/${uuid}`
|
||||||
|
|
||||||
const fileDir = `${backupsDir}/${uuid}`
|
|
||||||
const metaFilePath = `${fileDir}/${FileBackupsConstantsV1.MetadataFileName}`
|
const metaFilePath = `${fileDir}/${FileBackupsConstantsV1.MetadataFileName}`
|
||||||
const binaryPath = `${fileDir}/${FileBackupsConstantsV1.BinaryFileName}`
|
const binaryPath = `${fileDir}/${FileBackupsConstantsV1.BinaryFileName}`
|
||||||
|
|
||||||
@@ -184,25 +190,24 @@ export class FilesBackupManager implements FileBackupsDevice {
|
|||||||
const result = await downloader.run()
|
const result = await downloader.run()
|
||||||
|
|
||||||
if (result === 'success') {
|
if (result === 'success') {
|
||||||
const mapping = await this.getFilesBackupsMappingFile()
|
const mapping = await this.getFilesBackupsMappingFile(location)
|
||||||
|
|
||||||
mapping.files[uuid] = {
|
mapping.files[uuid] = {
|
||||||
backedUpOn: new Date(),
|
backedUpOn: new Date(),
|
||||||
absolutePath: fileDir,
|
|
||||||
relativePath: uuid,
|
relativePath: uuid,
|
||||||
metadataFileName: FileBackupsConstantsV1.MetadataFileName,
|
metadataFileName: FileBackupsConstantsV1.MetadataFileName,
|
||||||
binaryFileName: FileBackupsConstantsV1.BinaryFileName,
|
binaryFileName: FileBackupsConstantsV1.BinaryFileName,
|
||||||
version: FileBackupsConstantsV1.Version,
|
version: FileBackupsConstantsV1.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveFilesBackupsMappingFile(mapping)
|
await this.saveFilesBackupsMappingFile(location, mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
|
async getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
|
||||||
const operation = new FileReadOperation(record)
|
const operation = new FileReadOperation(filePath)
|
||||||
|
|
||||||
this.readOperations.set(operation.token, operation)
|
this.readOperations.set(operation.token, operation)
|
||||||
|
|
||||||
@@ -224,4 +229,180 @@ export class FilesBackupManager implements FileBackupsDevice {
|
|||||||
|
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { FileBackupReadChunkResponse, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
|
import { FileBackupReadChunkResponse } from '@web/Application/Device/DesktopSnjsExports'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const ONE_MB = 1024 * 1024
|
const ONE_MB = 1024 * 1024
|
||||||
const CHUNK_LIMIT = ONE_MB * 5
|
const CHUNK_LIMIT = ONE_MB * 5
|
||||||
@@ -11,9 +10,9 @@ export class FileReadOperation {
|
|||||||
private localFileId: number
|
private localFileId: number
|
||||||
private fileLength: number
|
private fileLength: number
|
||||||
|
|
||||||
constructor(backupRecord: FileBackupRecord) {
|
constructor(filePath: string) {
|
||||||
this.token = backupRecord.absolutePath
|
this.token = filePath
|
||||||
this.localFileId = fs.openSync(path.join(backupRecord.absolutePath, backupRecord.binaryFileName), 'r')
|
this.localFileId = fs.openSync(filePath, 'r')
|
||||||
this.fileLength = fs.fstatSync(this.localFileId).size
|
this.fileLength = fs.fstatSync(this.localFileId).size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { checkForUpdate, openChangelog, showUpdateInstallationDialog } from '../
|
|||||||
import { handleTestMessage } from '../Utils/Testing'
|
import { handleTestMessage } from '../Utils/Testing'
|
||||||
import { isDev, isTesting } from '../Utils/Utils'
|
import { isDev, isTesting } from '../Utils/Utils'
|
||||||
import { MessageType } from './../../../../test/TestIpcMessage'
|
import { MessageType } from './../../../../test/TestIpcMessage'
|
||||||
import { BackupsManagerInterface } from './../Backups/BackupsManagerInterface'
|
|
||||||
import { SpellcheckerManager } from './../SpellcheckerManager'
|
import { SpellcheckerManager } from './../SpellcheckerManager'
|
||||||
import { MenuManagerInterface } from './MenuManagerInterface'
|
import { MenuManagerInterface } from './MenuManagerInterface'
|
||||||
|
|
||||||
@@ -112,14 +111,12 @@ function suggestionsMenu(
|
|||||||
export function createMenuManager({
|
export function createMenuManager({
|
||||||
window,
|
window,
|
||||||
appState,
|
appState,
|
||||||
backupsManager,
|
|
||||||
trayManager,
|
trayManager,
|
||||||
store,
|
store,
|
||||||
spellcheckerManager,
|
spellcheckerManager,
|
||||||
}: {
|
}: {
|
||||||
window: Electron.BrowserWindow
|
window: Electron.BrowserWindow
|
||||||
appState: AppState
|
appState: AppState
|
||||||
backupsManager: BackupsManagerInterface
|
|
||||||
trayManager: TrayManager
|
trayManager: TrayManager
|
||||||
store: Store
|
store: Store
|
||||||
spellcheckerManager?: SpellcheckerManager
|
spellcheckerManager?: SpellcheckerManager
|
||||||
@@ -167,7 +164,6 @@ export function createMenuManager({
|
|||||||
editMenu(spellcheckerManager, reload),
|
editMenu(spellcheckerManager, reload),
|
||||||
viewMenu(window, store, reload),
|
viewMenu(window, store, reload),
|
||||||
windowMenu(store, trayManager, reload),
|
windowMenu(store, trayManager, reload),
|
||||||
backupsMenu(backupsManager, reload),
|
|
||||||
updateMenu(window, appState),
|
updateMenu(window, appState),
|
||||||
...(isLinux() ? [keyringMenu(window, store)] : []),
|
...(isLinux() ? [keyringMenu(window, store)] : []),
|
||||||
helpMenu(window, shell),
|
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) {
|
function updateMenu(window: BrowserWindow, appState: AppState) {
|
||||||
const updateState = appState.updates
|
const updateState = appState.updates
|
||||||
let label
|
let label
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ const rendererPath = path.join('file://', __dirname, '/renderer.js')
|
|||||||
import {
|
import {
|
||||||
FileBackupsDevice,
|
FileBackupsDevice,
|
||||||
FileBackupsMapping,
|
FileBackupsMapping,
|
||||||
FileBackupRecord,
|
|
||||||
FileBackupReadToken,
|
FileBackupReadToken,
|
||||||
FileBackupReadChunkResponse,
|
FileBackupReadChunkResponse,
|
||||||
|
PlaintextBackupsMapping,
|
||||||
} from '@web/Application/Device/DesktopSnjsExports'
|
} from '@web/Application/Device/DesktopSnjsExports'
|
||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
|
|
||||||
import { KeychainInterface } from '../Keychain/KeychainInterface'
|
import { KeychainInterface } from '../Keychain/KeychainInterface'
|
||||||
import { MenuManagerInterface } from '../Menus/MenuManagerInterface'
|
import { MenuManagerInterface } from '../Menus/MenuManagerInterface'
|
||||||
import { Component, PackageManagerInterface } from '../Packages/PackageManagerInterface'
|
import { Component, PackageManagerInterface } from '../Packages/PackageManagerInterface'
|
||||||
@@ -29,7 +28,6 @@ export class RemoteBridge implements CrossProcessBridge {
|
|||||||
constructor(
|
constructor(
|
||||||
private window: BrowserWindow,
|
private window: BrowserWindow,
|
||||||
private keychain: KeychainInterface,
|
private keychain: KeychainInterface,
|
||||||
private backups: BackupsManagerInterface,
|
|
||||||
private packages: PackageManagerInterface,
|
private packages: PackageManagerInterface,
|
||||||
private search: SearchManagerInterface,
|
private search: SearchManagerInterface,
|
||||||
private data: RemoteDataInterface,
|
private data: RemoteDataInterface,
|
||||||
@@ -54,28 +52,30 @@ export class RemoteBridge implements CrossProcessBridge {
|
|||||||
getKeychainValue: this.getKeychainValue.bind(this),
|
getKeychainValue: this.getKeychainValue.bind(this),
|
||||||
setKeychainValue: this.setKeychainValue.bind(this),
|
setKeychainValue: this.setKeychainValue.bind(this),
|
||||||
clearKeychainValue: this.clearKeychainValue.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),
|
displayAppMenu: this.displayAppMenu.bind(this),
|
||||||
saveDataBackup: this.saveDataBackup.bind(this),
|
|
||||||
syncComponents: this.syncComponents.bind(this),
|
syncComponents: this.syncComponents.bind(this),
|
||||||
onMajorDataChange: this.onMajorDataChange.bind(this),
|
|
||||||
onSearch: this.onSearch.bind(this),
|
onSearch: this.onSearch.bind(this),
|
||||||
onInitialDataLoad: this.onInitialDataLoad.bind(this),
|
|
||||||
destroyAllData: this.destroyAllData.bind(this),
|
destroyAllData: this.destroyAllData.bind(this),
|
||||||
getFilesBackupsMappingFile: this.getFilesBackupsMappingFile.bind(this),
|
getFilesBackupsMappingFile: this.getFilesBackupsMappingFile.bind(this),
|
||||||
saveFilesBackupsFile: this.saveFilesBackupsFile.bind(this),
|
saveFilesBackupsFile: this.saveFilesBackupsFile.bind(this),
|
||||||
isFilesBackupsEnabled: this.isFilesBackupsEnabled.bind(this),
|
isLegacyFilesBackupsEnabled: this.isLegacyFilesBackupsEnabled.bind(this),
|
||||||
enableFilesBackups: this.enableFilesBackups.bind(this),
|
getLegacyFilesBackupsLocation: this.getLegacyFilesBackupsLocation.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),
|
|
||||||
getFileBackupReadToken: this.getFileBackupReadToken.bind(this),
|
getFileBackupReadToken: this.getFileBackupReadToken.bind(this),
|
||||||
readNextChunk: this.readNextChunk.bind(this),
|
readNextChunk: this.readNextChunk.bind(this),
|
||||||
askForMediaAccess: this.askForMediaAccess.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()
|
return this.keychain.clearKeychainValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
async localBackupsCount() {
|
|
||||||
return this.backups.backupsCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewlocalBackups() {
|
|
||||||
this.backups.viewBackups()
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteLocalBackups() {
|
|
||||||
return this.backups.deleteBackups()
|
|
||||||
}
|
|
||||||
|
|
||||||
syncComponents(components: Component[]) {
|
syncComponents(components: Component[]) {
|
||||||
void this.packages.syncComponents(components)
|
void this.packages.syncComponents(components)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMajorDataChange() {
|
|
||||||
this.backups.performBackup()
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch(text: string) {
|
onSearch(text: string) {
|
||||||
this.search.findInPage(text)
|
this.search.findInPage(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
onInitialDataLoad() {
|
|
||||||
this.backups.beginBackups()
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyAllData() {
|
destroyAllData() {
|
||||||
this.data.destroySensitiveDirectories()
|
this.data.destroySensitiveDirectories()
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDataBackup(data: unknown) {
|
|
||||||
this.backups.saveBackupData(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
displayAppMenu() {
|
displayAppMenu() {
|
||||||
this.menus.popupMenu()
|
this.menus.popupMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilesBackupsMappingFile(): Promise<FileBackupsMapping> {
|
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping> {
|
||||||
return this.fileBackups.getFilesBackupsMappingFile()
|
return this.fileBackups.getFilesBackupsMappingFile(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
saveFilesBackupsFile(
|
saveFilesBackupsFile(
|
||||||
|
location: string,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
metaFile: string,
|
metaFile: string,
|
||||||
downloadRequest: {
|
downloadRequest: {
|
||||||
@@ -188,43 +165,74 @@ export class RemoteBridge implements CrossProcessBridge {
|
|||||||
url: string
|
url: string
|
||||||
},
|
},
|
||||||
): Promise<'success' | 'failed'> {
|
): Promise<'success' | 'failed'> {
|
||||||
return this.fileBackups.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
|
return this.fileBackups.saveFilesBackupsFile(location, uuid, metaFile, downloadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
|
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
|
||||||
return this.fileBackups.getFileBackupReadToken(record)
|
return this.fileBackups.getFileBackupReadToken(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
readNextChunk(nextToken: string): Promise<FileBackupReadChunkResponse> {
|
readNextChunk(nextToken: string): Promise<FileBackupReadChunkResponse> {
|
||||||
return this.fileBackups.readNextChunk(nextToken)
|
return this.fileBackups.readNextChunk(nextToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
public isFilesBackupsEnabled(): Promise<boolean> {
|
public isLegacyFilesBackupsEnabled(): Promise<boolean> {
|
||||||
return this.fileBackups.isFilesBackupsEnabled()
|
return this.fileBackups.isLegacyFilesBackupsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
public enableFilesBackups(): Promise<void> {
|
public getLegacyFilesBackupsLocation(): Promise<string | undefined> {
|
||||||
return this.fileBackups.enableFilesBackups()
|
return this.fileBackups.getLegacyFilesBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
public disableFilesBackups(): Promise<void> {
|
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
|
||||||
return this.fileBackups.disableFilesBackups()
|
return this.fileBackups.wasLegacyTextBackupsExplicitlyDisabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeFilesBackupsLocation(): Promise<string | undefined> {
|
getLegacyTextBackupsLocation(): Promise<string | undefined> {
|
||||||
return this.fileBackups.changeFilesBackupsLocation()
|
return this.fileBackups.getLegacyTextBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFilesBackupsLocation(): Promise<string> {
|
saveTextBackupData(location: string, data: string): Promise<void> {
|
||||||
return this.fileBackups.getFilesBackupsLocation()
|
return this.fileBackups.saveTextBackupData(location, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public openFilesBackupsLocation(): Promise<void> {
|
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void> {
|
||||||
return this.fileBackups.openFilesBackupsLocation()
|
return this.fileBackups.savePlaintextNoteBackup(location, uuid, name, tags, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public openFileBackup(record: FileBackupRecord): Promise<void> {
|
openLocation(path: string): Promise<void> {
|
||||||
return this.fileBackups.openFileBackup(record)
|
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> {
|
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {
|
||||||
|
|||||||
@@ -4,30 +4,34 @@ export enum StoreKeys {
|
|||||||
ExtServerHost = 'extServerHost',
|
ExtServerHost = 'extServerHost',
|
||||||
UseSystemMenuBar = 'useSystemMenuBar',
|
UseSystemMenuBar = 'useSystemMenuBar',
|
||||||
MenuBarVisible = 'isMenuBarVisible',
|
MenuBarVisible = 'isMenuBarVisible',
|
||||||
BackupsLocation = 'backupsLocation',
|
|
||||||
BackupsDisabled = 'backupsDisabled',
|
|
||||||
MinimizeToTray = 'minimizeToTray',
|
MinimizeToTray = 'minimizeToTray',
|
||||||
EnableAutoUpdate = 'enableAutoUpdates',
|
EnableAutoUpdate = 'enableAutoUpdates',
|
||||||
ZoomFactor = 'zoomFactor',
|
ZoomFactor = 'zoomFactor',
|
||||||
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
|
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
|
||||||
UseNativeKeychain = 'useNativeKeychain',
|
UseNativeKeychain = 'useNativeKeychain',
|
||||||
FileBackupsEnabled = 'fileBackupsEnabled',
|
|
||||||
FileBackupsLocation = 'fileBackupsLocation',
|
|
||||||
LastRunVersion = 'LastRunVersion',
|
LastRunVersion = 'LastRunVersion',
|
||||||
|
|
||||||
|
LegacyTextBackupsLocation = 'backupsLocation',
|
||||||
|
LegacyTextBackupsDisabled = 'backupsDisabled',
|
||||||
|
|
||||||
|
LegacyFileBackupsEnabled = 'fileBackupsEnabled',
|
||||||
|
LegacyFileBackupsLocation = 'fileBackupsLocation',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreData {
|
export interface StoreData {
|
||||||
[StoreKeys.ExtServerHost]: string
|
[StoreKeys.ExtServerHost]: string
|
||||||
[StoreKeys.UseSystemMenuBar]: boolean
|
[StoreKeys.UseSystemMenuBar]: boolean
|
||||||
[StoreKeys.MenuBarVisible]: boolean
|
[StoreKeys.MenuBarVisible]: boolean
|
||||||
[StoreKeys.BackupsLocation]: string
|
|
||||||
[StoreKeys.BackupsDisabled]: boolean
|
|
||||||
[StoreKeys.MinimizeToTray]: boolean
|
[StoreKeys.MinimizeToTray]: boolean
|
||||||
[StoreKeys.EnableAutoUpdate]: boolean
|
[StoreKeys.EnableAutoUpdate]: boolean
|
||||||
[StoreKeys.UseNativeKeychain]: boolean | null
|
[StoreKeys.UseNativeKeychain]: boolean | null
|
||||||
[StoreKeys.ZoomFactor]: number
|
[StoreKeys.ZoomFactor]: number
|
||||||
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
|
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
|
||||||
[StoreKeys.FileBackupsEnabled]: boolean
|
|
||||||
[StoreKeys.FileBackupsLocation]: string
|
|
||||||
[StoreKeys.LastRunVersion]: string
|
[StoreKeys.LastRunVersion]: string
|
||||||
|
|
||||||
|
[StoreKeys.LegacyTextBackupsLocation]: string
|
||||||
|
[StoreKeys.LegacyTextBackupsDisabled]: boolean
|
||||||
|
|
||||||
|
[StoreKeys.LegacyFileBackupsEnabled]: boolean
|
||||||
|
[StoreKeys.LegacyFileBackupsLocation]: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
import { BackupsDirectoryName } from '../Backups/BackupsManager'
|
|
||||||
import { Language } from '../SpellcheckerManager'
|
import { Language } from '../SpellcheckerManager'
|
||||||
import { FileDoesNotExist } from '../Utils/FileUtils'
|
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 { StoreData, StoreKeys } from './StoreKeys'
|
||||||
import { app, logError } from './Store'
|
import { logError } from './Store'
|
||||||
|
|
||||||
export function createSanitizedStoreData(data: any = {}): StoreData {
|
export function createSanitizedStoreData(data: any = {}): StoreData {
|
||||||
return {
|
return {
|
||||||
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
|
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
|
||||||
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
|
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
|
||||||
[StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false),
|
|
||||||
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
|
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
|
||||||
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
|
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
|
||||||
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
|
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
|
||||||
? data[StoreKeys.UseNativeKeychain]
|
? data[StoreKeys.UseNativeKeychain]
|
||||||
: null,
|
: null,
|
||||||
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
|
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
|
||||||
[StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]),
|
|
||||||
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
|
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
|
||||||
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
|
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
|
||||||
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
|
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
|
||||||
),
|
),
|
||||||
[StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false),
|
|
||||||
[StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation],
|
|
||||||
[StoreKeys.LastRunVersion]: data[StoreKeys.LastRunVersion],
|
[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 {
|
function sanitizeZoomFactor(factor?: any): number {
|
||||||
@@ -35,29 +34,7 @@ function sanitizeZoomFactor(factor?: any): number {
|
|||||||
return 1
|
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 {
|
function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set<Language> | null {
|
||||||
if (!languages) {
|
if (!languages) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export function createEnglishStrings(): Strings {
|
|||||||
automaticUpdatesDisabled: 'Automatic Updates Disabled',
|
automaticUpdatesDisabled: 'Automatic Updates Disabled',
|
||||||
disableAutomaticBackups: 'Disable Automatic Backups',
|
disableAutomaticBackups: 'Disable Automatic Backups',
|
||||||
enableAutomaticBackups: 'Enable Automatic Backups',
|
enableAutomaticBackups: 'Enable Automatic Backups',
|
||||||
changeBackupsLocation: 'Change Backups Location',
|
|
||||||
openBackupsLocation: 'Open Backups Location',
|
|
||||||
emailSupport: 'Email Support',
|
emailSupport: 'Email Support',
|
||||||
website: 'Website',
|
website: 'Website',
|
||||||
gitHub: 'GitHub',
|
gitHub: 'GitHub',
|
||||||
@@ -146,15 +144,5 @@ export function createEnglishStrings(): Strings {
|
|||||||
},
|
},
|
||||||
unknownVersionName: 'Unknown',
|
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)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,15 +28,5 @@ export function createFrenchStrings(): Strings {
|
|||||||
},
|
},
|
||||||
extensions: fallback.extensions,
|
extensions: fallback.extensions,
|
||||||
updates: fallback.updates,
|
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)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,6 @@ export function updates() {
|
|||||||
return str().updates
|
return str().updates
|
||||||
}
|
}
|
||||||
|
|
||||||
export function backups() {
|
|
||||||
return str().backups
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringsForLocale(locale: string): Strings {
|
function stringsForLocale(locale: string): Strings {
|
||||||
if (locale === 'en' || locale.startsWith('en-')) {
|
if (locale === 'en' || locale.startsWith('en-')) {
|
||||||
return createEnglishStrings()
|
return createEnglishStrings()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export interface Strings {
|
|||||||
tray: TrayStrings
|
tray: TrayStrings
|
||||||
extensions: ExtensionsStrings
|
extensions: ExtensionsStrings
|
||||||
updates: UpdateStrings
|
updates: UpdateStrings
|
||||||
backups: BackupsStrings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppMenuStrings {
|
interface AppMenuStrings {
|
||||||
@@ -18,8 +17,6 @@ interface AppMenuStrings {
|
|||||||
automaticUpdatesDisabled: string
|
automaticUpdatesDisabled: string
|
||||||
disableAutomaticBackups: string
|
disableAutomaticBackups: string
|
||||||
enableAutomaticBackups: string
|
enableAutomaticBackups: string
|
||||||
changeBackupsLocation: string
|
|
||||||
openBackupsLocation: string
|
|
||||||
emailSupport: string
|
emailSupport: string
|
||||||
website: string
|
website: string
|
||||||
gitHub: string
|
gitHub: string
|
||||||
@@ -103,7 +100,3 @@ interface UpdateStrings {
|
|||||||
}
|
}
|
||||||
unknownVersionName: string
|
unknownVersionName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackupsStrings {
|
|
||||||
errorChangingDirectory(error: any): string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const Paths = {
|
|||||||
get userDataDir(): string {
|
get userDataDir(): string {
|
||||||
return app.getPath('userData')
|
return app.getPath('userData')
|
||||||
},
|
},
|
||||||
|
get homeDir(): string {
|
||||||
|
return app.getPath('home')
|
||||||
|
},
|
||||||
get documentsDir(): string {
|
get documentsDir(): string {
|
||||||
return app.getPath('documents')
|
return app.getPath('documents')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { compareVersions } from 'compare-versions'
|
|||||||
import { BrowserWindow, dialog, shell } from 'electron'
|
import { BrowserWindow, dialog, shell } from 'electron'
|
||||||
import electronLog from 'electron-log'
|
import electronLog from 'electron-log'
|
||||||
import { autoUpdater } from 'electron-updater'
|
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 { MessageType } from '../../../test/TestIpcMessage'
|
||||||
import { AppState } from '../../AppState'
|
import { AppState } from '../../AppState'
|
||||||
import { MessageToWebApp } from '../Shared/IpcMessages'
|
import { MessageToWebApp } from '../Shared/IpcMessages'
|
||||||
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
|
|
||||||
import { StoreKeys } from './Store/StoreKeys'
|
import { StoreKeys } from './Store/StoreKeys'
|
||||||
import { updates as str } from './Strings'
|
import { updates as str } from './Strings'
|
||||||
import { autoUpdatingAvailable } from './Types/Constants'
|
import { autoUpdatingAvailable } from './Types/Constants'
|
||||||
@@ -84,7 +83,7 @@ export class UpdateState {
|
|||||||
|
|
||||||
let updatesSetup = false
|
let updatesSetup = false
|
||||||
|
|
||||||
export function setupUpdates(window: BrowserWindow, appState: AppState, backupsManager: BackupsManagerInterface): void {
|
export function setupUpdates(window: BrowserWindow, appState: AppState): void {
|
||||||
if (!autoUpdatingAvailable) {
|
if (!autoUpdatingAvailable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -97,22 +96,6 @@ export function setupUpdates(window: BrowserWindow, appState: AppState, backupsM
|
|||||||
|
|
||||||
const updateState = appState.updates
|
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 }) => {
|
autoUpdater.on('update-downloaded', (info: { version?: string }) => {
|
||||||
window.webContents.send(MessageToWebApp.UpdateAvailable, null)
|
window.webContents.send(MessageToWebApp.UpdateAvailable, null)
|
||||||
updateState.autoUpdateHasBeenDownloaded(info.version || 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 }) => {
|
autoUpdater.on(MessageToWebApp.UpdateAvailable, (info: { version?: string }) => {
|
||||||
updateState.checkedForUpdate(info.version || null)
|
updateState.checkedForUpdate(info.version || null)
|
||||||
if (updateState.enableAutoUpdate) {
|
if (updateState.enableAutoUpdate) {
|
||||||
const canUpdate = checkUpdateSafety()
|
const canUpdate = updateState.enableAutoUpdate
|
||||||
if (!canUpdate) {
|
autoUpdater.autoInstallOnAppQuit = canUpdate
|
||||||
backupsManager.performBackup()
|
autoUpdater.autoDownload = canUpdate
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
autoUpdater.on('update-not-available', (info: { version?: string }) => {
|
autoUpdater.on('update-not-available', (info: { version?: string }) => {
|
||||||
@@ -164,46 +146,21 @@ function quitAndInstall(window: BrowserWindow) {
|
|||||||
}, 0)
|
}, 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> {
|
export async function showUpdateInstallationDialog(parentWindow: BrowserWindow, appState: AppState): Promise<void> {
|
||||||
if (!appState.updates.latestVersion) {
|
if (!appState.updates.latestVersion) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.lastBackupDate && isLessThanOneHourFromNow(appState.lastBackupDate)) {
|
const result = await dialog.showMessageBox(parentWindow, {
|
||||||
const result = await dialog.showMessageBox(parentWindow, {
|
type: 'info',
|
||||||
type: 'info',
|
title: str().updateReady.title,
|
||||||
title: str().updateReady.title,
|
message: str().updateReady.message(appState.updates.latestVersion),
|
||||||
message: str().updateReady.message(appState.updates.latestVersion),
|
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
|
||||||
buttons: [str().updateReady.installLater, str().updateReady.quitAndInstall],
|
cancelId: 0,
|
||||||
cancelId: 0,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const buttonIndex = result.response
|
const buttonIndex = result.response
|
||||||
if (buttonIndex === 1) {
|
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
|
|
||||||
}
|
|
||||||
quitAndInstall(parentWindow)
|
quitAndInstall(parentWindow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ export function debouncedJSONDiskWriter(durationMs: number, location: string, da
|
|||||||
}, durationMs)
|
}, durationMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openDirectoryPicker(): Promise<string | undefined> {
|
export async function openDirectoryPicker(buttonLabel?: string): Promise<string | undefined> {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'],
|
properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'],
|
||||||
|
buttonLabel: buttonLabel,
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.filePaths[0]
|
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')
|
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> {
|
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const stat = await fs.promises.lstat(dirPath)
|
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)))))
|
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 {
|
try {
|
||||||
await fs.promises.rename(source, destination)
|
await fs.promises.rename(source, destination)
|
||||||
} catch (_error) {
|
} 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. */
|
/** Deletes a file, handling EPERM and EBUSY errors on Windows. */
|
||||||
export async function deleteFile(filePath: PathLike): Promise<void> {
|
export async function deleteFile(filePath: PathLike): Promise<void> {
|
||||||
for (let i = 1, maxTries = 10; i < maxTries; i++) {
|
for (let i = 1, maxTries = 10; i < maxTries; i++) {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import path from 'path'
|
|||||||
import { AppMessageType, MessageType } from '../../../test/TestIpcMessage'
|
import { AppMessageType, MessageType } from '../../../test/TestIpcMessage'
|
||||||
import { AppState } from '../../AppState'
|
import { AppState } from '../../AppState'
|
||||||
import { MessageToWebApp } from '../Shared/IpcMessages'
|
import { MessageToWebApp } from '../Shared/IpcMessages'
|
||||||
import { createBackupsManager } from './Backups/BackupsManager'
|
|
||||||
import { BackupsManagerInterface } from './Backups/BackupsManagerInterface'
|
|
||||||
import { FilesBackupManager } from './FileBackups/FileBackupsManager'
|
import { FilesBackupManager } from './FileBackups/FileBackupsManager'
|
||||||
import { Keychain } from './Keychain/Keychain'
|
import { Keychain } from './Keychain/Keychain'
|
||||||
import { MediaManager } from './Media/MediaManager'
|
import { MediaManager } from './Media/MediaManager'
|
||||||
@@ -35,7 +33,6 @@ const WINDOW_MIN_HEIGHT = 400
|
|||||||
export interface WindowState {
|
export interface WindowState {
|
||||||
window: Electron.BrowserWindow
|
window: Electron.BrowserWindow
|
||||||
menuManager: MenuManagerInterface
|
menuManager: MenuManagerInterface
|
||||||
backupsManager: BackupsManagerInterface
|
|
||||||
trayManager: TrayManager
|
trayManager: TrayManager
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +61,6 @@ export async function createWindowState({
|
|||||||
;(global as any).RemoteBridge = new RemoteBridge(
|
;(global as any).RemoteBridge = new RemoteBridge(
|
||||||
window,
|
window,
|
||||||
Keychain,
|
Keychain,
|
||||||
services.backupsManager,
|
|
||||||
services.packageManager,
|
services.packageManager,
|
||||||
services.searchManager,
|
services.searchManager,
|
||||||
{
|
{
|
||||||
@@ -93,7 +89,6 @@ export async function createWindowState({
|
|||||||
|
|
||||||
window.on('blur', () => {
|
window.on('blur', () => {
|
||||||
window.webContents.send(MessageToWebApp.WindowBlurred, null)
|
window.webContents.send(MessageToWebApp.WindowBlurred, null)
|
||||||
services.backupsManager.applicationDidBlur()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
window.once('ready-to-show', () => {
|
window.once('ready-to-show', () => {
|
||||||
@@ -201,8 +196,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
|||||||
const searchManager = initializeSearchManager(window.webContents)
|
const searchManager = initializeSearchManager(window.webContents)
|
||||||
initializeZoomManager(window, appState.store)
|
initializeZoomManager(window, appState.store)
|
||||||
|
|
||||||
const backupsManager = createBackupsManager(window.webContents, appState)
|
const updateManager = setupUpdates(window, appState)
|
||||||
const updateManager = setupUpdates(window, appState, backupsManager)
|
|
||||||
const trayManager = createTrayManager(window, appState.store)
|
const trayManager = createTrayManager(window, appState.store)
|
||||||
const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale)
|
const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale)
|
||||||
const mediaManager = new MediaManager()
|
const mediaManager = new MediaManager()
|
||||||
@@ -214,16 +208,14 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
|||||||
const menuManager = createMenuManager({
|
const menuManager = createMenuManager({
|
||||||
appState,
|
appState,
|
||||||
window,
|
window,
|
||||||
backupsManager,
|
|
||||||
trayManager,
|
trayManager,
|
||||||
store: appState.store,
|
store: appState.store,
|
||||||
spellcheckerManager,
|
spellcheckerManager,
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileBackupsManager = new FilesBackupManager(appState)
|
const fileBackupsManager = new FilesBackupManager(appState, window.webContents)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backupsManager,
|
|
||||||
updateManager,
|
updateManager,
|
||||||
trayManager,
|
trayManager,
|
||||||
spellcheckerManager,
|
spellcheckerManager,
|
||||||
|
|||||||
@@ -3,52 +3,22 @@ import { Component } from '../Main/Packages/PackageManagerInterface'
|
|||||||
|
|
||||||
export interface CrossProcessBridge extends FileBackupsDevice {
|
export interface CrossProcessBridge extends FileBackupsDevice {
|
||||||
get extServerHost(): string
|
get extServerHost(): string
|
||||||
|
|
||||||
get useNativeKeychain(): boolean
|
get useNativeKeychain(): boolean
|
||||||
|
|
||||||
get rendererPath(): string
|
get rendererPath(): string
|
||||||
|
|
||||||
get isMacOS(): boolean
|
get isMacOS(): boolean
|
||||||
|
|
||||||
get appVersion(): string
|
get appVersion(): string
|
||||||
|
|
||||||
get useSystemMenuBar(): boolean
|
get useSystemMenuBar(): boolean
|
||||||
|
|
||||||
closeWindow(): void
|
closeWindow(): void
|
||||||
|
|
||||||
minimizeWindow(): void
|
minimizeWindow(): void
|
||||||
|
|
||||||
maximizeWindow(): void
|
maximizeWindow(): void
|
||||||
|
|
||||||
unmaximizeWindow(): void
|
unmaximizeWindow(): void
|
||||||
|
|
||||||
isWindowMaximized(): boolean
|
isWindowMaximized(): boolean
|
||||||
|
|
||||||
getKeychainValue(): Promise<unknown>
|
getKeychainValue(): Promise<unknown>
|
||||||
|
|
||||||
setKeychainValue: (value: unknown) => Promise<void>
|
setKeychainValue: (value: unknown) => Promise<void>
|
||||||
|
|
||||||
clearKeychainValue(): Promise<boolean>
|
clearKeychainValue(): Promise<boolean>
|
||||||
|
|
||||||
localBackupsCount(): Promise<number>
|
|
||||||
|
|
||||||
viewlocalBackups(): void
|
|
||||||
|
|
||||||
deleteLocalBackups(): Promise<void>
|
|
||||||
|
|
||||||
saveDataBackup(data: unknown): void
|
|
||||||
|
|
||||||
displayAppMenu(): void
|
displayAppMenu(): void
|
||||||
|
|
||||||
syncComponents(components: Component[]): void
|
syncComponents(components: Component[]): void
|
||||||
|
|
||||||
onMajorDataChange(): void
|
|
||||||
|
|
||||||
onSearch(text: string): void
|
onSearch(text: string): void
|
||||||
|
|
||||||
onInitialDataLoad(): void
|
|
||||||
|
|
||||||
destroyAllData(): void
|
destroyAllData(): void
|
||||||
|
|
||||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
|
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
DesktopDeviceInterface,
|
DesktopDeviceInterface,
|
||||||
Environment,
|
Environment,
|
||||||
FileBackupsMapping,
|
|
||||||
RawKeychainValue,
|
RawKeychainValue,
|
||||||
FileBackupRecord,
|
|
||||||
FileBackupReadToken,
|
FileBackupReadToken,
|
||||||
FileBackupReadChunkResponse,
|
FileBackupReadChunkResponse,
|
||||||
|
FileBackupsMapping,
|
||||||
|
PlaintextBackupsMapping,
|
||||||
} from '@web/Application/Device/DesktopSnjsExports'
|
} from '@web/Application/Device/DesktopSnjsExports'
|
||||||
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
|
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
|
||||||
import { Component } from '../Main/Packages/PackageManagerInterface'
|
import { Component } from '../Main/Packages/PackageManagerInterface'
|
||||||
@@ -25,6 +25,33 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
|||||||
super(appVersion)
|
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() {
|
async getKeychainValue() {
|
||||||
if (this.useNativeKeychain) {
|
if (this.useNativeKeychain) {
|
||||||
const keychainValue = await this.remoteBridge.getKeychainValue()
|
const keychainValue = await this.remoteBridge.getKeychainValue()
|
||||||
@@ -57,18 +84,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
|||||||
this.remoteBridge.syncComponents(components)
|
this.remoteBridge.syncComponents(components)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMajorDataChange() {
|
|
||||||
this.remoteBridge.onMajorDataChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch(text: string) {
|
onSearch(text: string) {
|
||||||
this.remoteBridge.onSearch(text)
|
this.remoteBridge.onSearch(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
onInitialDataLoad() {
|
|
||||||
this.remoteBridge.onInitialDataLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearAllDataFromDevice(workspaceIdentifiers: string[]): Promise<{ killsApplication: boolean }> {
|
async clearAllDataFromDevice(workspaceIdentifiers: string[]): Promise<{ killsApplication: boolean }> {
|
||||||
await super.clearAllDataFromDevice(workspaceIdentifiers)
|
await super.clearAllDataFromDevice(workspaceIdentifiers)
|
||||||
|
|
||||||
@@ -77,69 +96,36 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
|||||||
return { killsApplication: true }
|
return { killsApplication: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBackup() {
|
public isLegacyFilesBackupsEnabled(): Promise<boolean> {
|
||||||
const receiver = window.webClient
|
return this.remoteBridge.isLegacyFilesBackupsEnabled()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async localBackupsCount() {
|
public getLegacyFilesBackupsLocation(): Promise<string | undefined> {
|
||||||
return this.remoteBridge.localBackupsCount()
|
return this.remoteBridge.getLegacyFilesBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewlocalBackups() {
|
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean> {
|
||||||
this.remoteBridge.viewlocalBackups()
|
return this.remoteBridge.wasLegacyTextBackupsExplicitlyDisabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteLocalBackups() {
|
getUserDocumentsDirectory(): Promise<string> {
|
||||||
return this.remoteBridge.deleteLocalBackups()
|
return this.remoteBridge.getUserDocumentsDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
public isFilesBackupsEnabled(): Promise<boolean> {
|
getLegacyTextBackupsLocation(): Promise<string | undefined> {
|
||||||
return this.remoteBridge.isFilesBackupsEnabled()
|
return this.remoteBridge.getLegacyTextBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
public enableFilesBackups(): Promise<void> {
|
saveTextBackupData(workspaceId: string, data: string): Promise<void> {
|
||||||
return this.remoteBridge.enableFilesBackups()
|
return this.remoteBridge.saveTextBackupData(workspaceId, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public disableFilesBackups(): Promise<void> {
|
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void> {
|
||||||
return this.remoteBridge.disableFilesBackups()
|
return this.remoteBridge.savePlaintextNoteBackup(location, uuid, name, tags, data)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveFilesBackupsFile(
|
async saveFilesBackupsFile(
|
||||||
|
location: string,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
metaFile: string,
|
metaFile: string,
|
||||||
downloadRequest: {
|
downloadRequest: {
|
||||||
@@ -148,17 +134,25 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
|||||||
url: string
|
url: string
|
||||||
},
|
},
|
||||||
): Promise<'success' | 'failed'> {
|
): Promise<'success' | 'failed'> {
|
||||||
return this.remoteBridge.saveFilesBackupsFile(uuid, metaFile, downloadRequest)
|
return this.remoteBridge.saveFilesBackupsFile(location, uuid, metaFile, downloadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
|
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken> {
|
||||||
return this.remoteBridge.getFileBackupReadToken(record)
|
return this.remoteBridge.getFileBackupReadToken(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void> {
|
||||||
|
return this.remoteBridge.migrateLegacyFileBackupsToNewStructure(newPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
readNextChunk(token: string): Promise<FileBackupReadChunkResponse> {
|
readNextChunk(token: string): Promise<FileBackupReadChunkResponse> {
|
||||||
return this.remoteBridge.readNextChunk(token)
|
return this.remoteBridge.readNextChunk(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void> {
|
||||||
|
return this.remoteBridge.monitorPlaintextBackupsLocationForChanges(backupsDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
async performHardReset(): Promise<void> {
|
async performHardReset(): Promise<void> {
|
||||||
console.error('performHardReset is not yet implemented')
|
console.error('performHardReset is not yet implemented')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,25 @@
|
|||||||
import { IpcRendererEvent } from 'electron/renderer'
|
|
||||||
import { MessageToWebApp } from '../Shared/IpcMessages'
|
import { MessageToWebApp } from '../Shared/IpcMessages'
|
||||||
|
import { ElectronMainEvents, MainEventHandler } from '../Shared/ElectronMainEvents'
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
const RemoteBridge = require('@electron/remote').getGlobal('RemoteBridge')
|
const RemoteBridge = require('@electron/remote').getGlobal('RemoteBridge')
|
||||||
const { contextBridge } = require('electron')
|
const { contextBridge } = require('electron')
|
||||||
|
|
||||||
type MainEventCallback = (event: IpcRendererEvent, value: any) => void
|
|
||||||
|
|
||||||
process.once('loaded', function () {
|
process.once('loaded', function () {
|
||||||
contextBridge.exposeInMainWorld('electronRemoteBridge', RemoteBridge.exposableValue)
|
contextBridge.exposeInMainWorld('electronRemoteBridge', RemoteBridge.exposableValue)
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronMainEvents', {
|
const mainEvents: ElectronMainEvents = {
|
||||||
handleUpdateAvailable: (callback: MainEventCallback) => ipcRenderer.on(MessageToWebApp.UpdateAvailable, callback),
|
setUpdateAvailableHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.UpdateAvailable, handler),
|
||||||
|
|
||||||
handlePerformAutomatedBackup: (callback: MainEventCallback) =>
|
setWindowBlurredHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.WindowBlurred, handler),
|
||||||
ipcRenderer.on(MessageToWebApp.PerformAutomatedBackup, callback),
|
|
||||||
|
|
||||||
handleFinishedSavingBackup: (callback: MainEventCallback) =>
|
setWindowFocusedHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.WindowFocused, handler),
|
||||||
ipcRenderer.on(MessageToWebApp.FinishedSavingBackup, callback),
|
|
||||||
|
|
||||||
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) =>
|
contextBridge.exposeInMainWorld('electronMainEvents', mainEvents)
|
||||||
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, callback),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 { StartApplication } from '@web/Application/Device/StartApplication'
|
||||||
import { IpcRendererEvent } from 'electron/renderer'
|
import { IpcRendererEvent } from 'electron/renderer'
|
||||||
import { CrossProcessBridge } from './CrossProcessBridge'
|
import { CrossProcessBridge } from './CrossProcessBridge'
|
||||||
import { DesktopDevice } from './DesktopDevice'
|
import { DesktopDevice } from './DesktopDevice'
|
||||||
|
import { ElectronMainEvents } from '../Shared/ElectronMainEvents'
|
||||||
|
|
||||||
declare const DEFAULT_SYNC_SERVER: string
|
declare const DEFAULT_SYNC_SERVER: string
|
||||||
declare const WEBSOCKET_URL: string
|
declare const WEBSOCKET_URL: string
|
||||||
@@ -23,7 +27,7 @@ declare global {
|
|||||||
purchaseUrl: string
|
purchaseUrl: string
|
||||||
startApplication: StartApplication
|
startApplication: StartApplication
|
||||||
zip: unknown
|
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.webClient.updateAvailable()
|
||||||
})
|
})
|
||||||
|
|
||||||
window.electronMainEvents.handlePerformAutomatedBackup(() => {
|
window.electronMainEvents.setWindowBlurredHandler(() => {
|
||||||
void window.device.downloadBackup()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.electronMainEvents.handleFinishedSavingBackup((_: IpcRendererEvent, data: { success: boolean }) => {
|
|
||||||
window.webClient.didFinishBackup(data.success)
|
|
||||||
})
|
|
||||||
|
|
||||||
window.electronMainEvents.handleWindowBlurred(() => {
|
|
||||||
window.webClient.windowLostFocus()
|
window.webClient.windowLostFocus()
|
||||||
})
|
})
|
||||||
|
|
||||||
window.electronMainEvents.handleWindowFocused(() => {
|
window.electronMainEvents.setWindowFocusedHandler(() => {
|
||||||
window.webClient.windowGainedFocus()
|
window.webClient.windowGainedFocus()
|
||||||
})
|
})
|
||||||
|
|
||||||
window.electronMainEvents.handleInstallComponentComplete((_: IpcRendererEvent, data: any) => {
|
window.electronMainEvents.setInstallComponentCompleteHandler((_: IpcRendererEvent, data: any) => {
|
||||||
void window.webClient.onComponentInstallationComplete(data.component, undefined)
|
void window.webClient.onComponentInstallationComplete(data.component, undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.electronMainEvents.setWatchedDirectoriesChangeHandler((_: IpcRendererEvent, changes: unknown) => {
|
||||||
|
void window.webClient.handleWatchedDirectoriesChanges(changes as DesktopWatchedDirectoriesChanges)
|
||||||
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
export enum MessageToWebApp {
|
export enum MessageToWebApp {
|
||||||
UpdateAvailable = 'update-available',
|
UpdateAvailable = 'update-available',
|
||||||
PerformAutomatedBackup = 'download-backup',
|
|
||||||
FinishedSavingBackup = 'finished-saving-backup',
|
|
||||||
WindowBlurred = 'window-blurred',
|
WindowBlurred = 'window-blurred',
|
||||||
WindowFocused = 'window-focused',
|
WindowFocused = 'window-focused',
|
||||||
InstallComponentComplete = 'install-component-complete',
|
InstallComponentComplete = 'install-component-complete',
|
||||||
|
WatchedDirectoriesChanges = 'watched-directories-changes',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MessageToMainProcess {
|
export enum MessageToMainProcess {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint:eslint": "eslint app/index.ts app/application.ts app/javascripts/**/*.ts",
|
"lint:eslint": "eslint app/index.ts app/application.ts app/javascripts/**/*.ts",
|
||||||
"lint:formatting": "prettier --check app",
|
"lint:formatting": "prettier --check app",
|
||||||
"lint": "yarn lint:formatting && yarn lint:eslint app",
|
"lint": "yarn lint:formatting && yarn lint:eslint app && yarn tsc",
|
||||||
"tsc": "tsc --noEmit",
|
"tsc": "tsc --noEmit",
|
||||||
"release:mac": "node scripts/build.mjs mac",
|
"release:mac": "node scripts/build.mjs mac",
|
||||||
"start": "electron ./app --enable-logging --icon _icon/icon.png",
|
"start": "electron ./app --enable-logging --icon _icon/icon.png",
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type DesktopWatchedDirectoriesChange = {
|
||||||
|
itemUuid: string
|
||||||
|
path: string
|
||||||
|
type: 'rename' | 'change'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DesktopWatchedDirectoriesChanges = DesktopWatchedDirectoriesChange[]
|
||||||
@@ -1,12 +1,44 @@
|
|||||||
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
|
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
|
||||||
import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping'
|
import { FileBackupsMapping } from './FileBackupsMapping'
|
||||||
|
|
||||||
|
type PlaintextNoteRecord = {
|
||||||
|
tag?: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UuidString = string
|
||||||
|
export type PlaintextBackupsMapping = {
|
||||||
|
version: string
|
||||||
|
files: Record<UuidString, PlaintextNoteRecord[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileBackupsDevice
|
||||||
|
extends FileBackupsMethods,
|
||||||
|
LegacyBackupsMethods,
|
||||||
|
PlaintextBackupsMethods,
|
||||||
|
TextBackupsMethods {
|
||||||
|
openLocation(path: string): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we combine presenting a directory picker and transfering old files to the new location
|
||||||
|
* in one function is so we don't have to expose a general `transferDirectories` function to the web app,
|
||||||
|
* which would give it too much power.
|
||||||
|
* @param appendPath The path to append to the selected directory.
|
||||||
|
*/
|
||||||
|
presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
|
appendPath: string,
|
||||||
|
oldLocation?: string,
|
||||||
|
): Promise<string | undefined>
|
||||||
|
|
||||||
|
monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
export type FileBackupReadToken = string
|
export type FileBackupReadToken = string
|
||||||
export type FileBackupReadChunkResponse = { chunk: Uint8Array; isLast: boolean; progress: FileDownloadProgress }
|
export type FileBackupReadChunkResponse = { chunk: Uint8Array; isLast: boolean; progress: FileDownloadProgress }
|
||||||
|
interface FileBackupsMethods {
|
||||||
export interface FileBackupsDevice {
|
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping>
|
||||||
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
|
|
||||||
saveFilesBackupsFile(
|
saveFilesBackupsFile(
|
||||||
|
location: string,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
metaFile: string,
|
metaFile: string,
|
||||||
downloadRequest: {
|
downloadRequest: {
|
||||||
@@ -15,13 +47,26 @@ export interface FileBackupsDevice {
|
|||||||
url: string
|
url: string
|
||||||
},
|
},
|
||||||
): Promise<'success' | 'failed'>
|
): Promise<'success' | 'failed'>
|
||||||
getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken>
|
getFileBackupReadToken(filePath: string): Promise<FileBackupReadToken>
|
||||||
readNextChunk(token: string): Promise<FileBackupReadChunkResponse>
|
readNextChunk(token: string): Promise<FileBackupReadChunkResponse>
|
||||||
isFilesBackupsEnabled(): Promise<boolean>
|
}
|
||||||
enableFilesBackups(): Promise<void>
|
|
||||||
disableFilesBackups(): Promise<void>
|
interface PlaintextBackupsMethods {
|
||||||
changeFilesBackupsLocation(): Promise<string | undefined>
|
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping>
|
||||||
getFilesBackupsLocation(): Promise<string>
|
persistPlaintextBackupsMappingFile(location: string): Promise<void>
|
||||||
openFilesBackupsLocation(): Promise<void>
|
savePlaintextNoteBackup(location: string, uuid: string, name: string, tags: string[], data: string): Promise<void>
|
||||||
openFileBackup(record: FileBackupRecord): Promise<void>
|
}
|
||||||
|
|
||||||
|
interface TextBackupsMethods {
|
||||||
|
getTextBackupsCount(location: string): Promise<number>
|
||||||
|
saveTextBackupData(location: string, data: string): Promise<void>
|
||||||
|
getUserDocumentsDirectory(): Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LegacyBackupsMethods {
|
||||||
|
migrateLegacyFileBackupsToNewStructure(newPath: string): Promise<void>
|
||||||
|
isLegacyFilesBackupsEnabled(): Promise<boolean>
|
||||||
|
getLegacyFilesBackupsLocation(): Promise<string | undefined>
|
||||||
|
wasLegacyTextBackupsExplicitlyDisabled(): Promise<boolean>
|
||||||
|
getLegacyTextBackupsLocation(): Promise<string | undefined>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'
|
|||||||
|
|
||||||
export type FileBackupRecord = {
|
export type FileBackupRecord = {
|
||||||
backedUpOn: Date
|
backedUpOn: Date
|
||||||
absolutePath: string
|
|
||||||
relativePath: string
|
relativePath: string
|
||||||
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
|
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
|
||||||
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
|
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
|
||||||
|
|||||||
@@ -1,14 +1,35 @@
|
|||||||
import { OnChunkCallback } from '../Chunker/OnChunkCallback'
|
import { OnChunkCallback } from '../Chunker/OnChunkCallback'
|
||||||
|
import { DesktopWatchedDirectoriesChanges } from '../Device/DesktopWatchedChanges'
|
||||||
import { FileBackupRecord } from '../Device/FileBackupsMapping'
|
import { FileBackupRecord } from '../Device/FileBackupsMapping'
|
||||||
|
|
||||||
export interface BackupServiceInterface {
|
export interface BackupServiceInterface {
|
||||||
|
openAllDirectoriesContainingBackupFiles(): void
|
||||||
|
prependWorkspacePathForPath(path: string): string
|
||||||
|
importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void>
|
||||||
|
|
||||||
getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined>
|
getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined>
|
||||||
readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'>
|
readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'>
|
||||||
isFilesBackupsEnabled(): Promise<boolean>
|
isFilesBackupsEnabled(): boolean
|
||||||
enableFilesBackups(): Promise<void>
|
enableFilesBackups(): Promise<void>
|
||||||
disableFilesBackups(): Promise<void>
|
disableFilesBackups(): void
|
||||||
changeFilesBackupsLocation(): Promise<string | undefined>
|
changeFilesBackupsLocation(): Promise<string | undefined>
|
||||||
getFilesBackupsLocation(): Promise<string>
|
getFilesBackupsLocation(): string | undefined
|
||||||
openFilesBackupsLocation(): Promise<void>
|
openFilesBackupsLocation(): Promise<void>
|
||||||
openFileBackup(record: FileBackupRecord): Promise<void>
|
openFileBackup(record: FileBackupRecord): Promise<void>
|
||||||
|
getFileBackupAbsolutePath(record: FileBackupRecord): string
|
||||||
|
|
||||||
|
isTextBackupsEnabled(): boolean
|
||||||
|
enableTextBackups(): Promise<void>
|
||||||
|
disableTextBackups(): void
|
||||||
|
getTextBackupsLocation(): string | undefined
|
||||||
|
openTextBackupsLocation(): Promise<void>
|
||||||
|
changeTextBackupsLocation(): Promise<string | undefined>
|
||||||
|
saveTextBackupData(data: string): Promise<void>
|
||||||
|
|
||||||
|
isPlaintextBackupsEnabled(): boolean
|
||||||
|
enablePlaintextBackups(): Promise<void>
|
||||||
|
disablePlaintextBackups(): void
|
||||||
|
getPlaintextBackupsLocation(): string | undefined
|
||||||
|
openPlaintextBackupsLocation(): Promise<void>
|
||||||
|
changePlaintextBackupsLocation(): Promise<string | undefined>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
export * from './Api/DirectoryHandle'
|
export * from './Api/DirectoryHandle'
|
||||||
export * from './Api/FileHandleRead'
|
export * from './Api/FileHandleRead'
|
||||||
export * from './Api/FileHandleReadWrite'
|
export * from './Api/FileHandleReadWrite'
|
||||||
|
export * from './Api/FilesApiInterface'
|
||||||
export * from './Api/FileSystemApi'
|
export * from './Api/FileSystemApi'
|
||||||
export * from './Api/FileSystemNoSelection'
|
export * from './Api/FileSystemNoSelection'
|
||||||
export * from './Api/FileSystemResult'
|
export * from './Api/FileSystemResult'
|
||||||
export * from './Api/FilesApiInterface'
|
|
||||||
export * from './Cache/FileMemoryCache'
|
export * from './Cache/FileMemoryCache'
|
||||||
export * from './Chunker/ByteChunker'
|
export * from './Chunker/ByteChunker'
|
||||||
export * from './Chunker/OnChunkCallback'
|
export * from './Chunker/OnChunkCallback'
|
||||||
export * from './Chunker/OrderedByteChunker'
|
export * from './Chunker/OrderedByteChunker'
|
||||||
|
export * from './Device/DesktopWatchedChanges'
|
||||||
export * from './Device/FileBackupMetadataFile'
|
export * from './Device/FileBackupMetadataFile'
|
||||||
export * from './Device/FileBackupsConstantsV1'
|
export * from './Device/FileBackupsConstantsV1'
|
||||||
export * from './Device/FileBackupsDevice'
|
export * from './Device/FileBackupsDevice'
|
||||||
export * from './Device/FileBackupsMapping'
|
export * from './Device/FileBackupsMapping'
|
||||||
|
export * from './Operations/DownloadAndDecrypt'
|
||||||
|
export * from './Operations/EncryptAndUpload'
|
||||||
export * from './Service/BackupServiceInterface'
|
export * from './Service/BackupServiceInterface'
|
||||||
export * from './Service/FilesClientInterface'
|
export * from './Service/FilesClientInterface'
|
||||||
export * from './Service/ReadAndDecryptBackupFileFileSystemAPI'
|
export * from './Service/ReadAndDecryptBackupFileFileSystemAPI'
|
||||||
export * from './Service/ReadAndDecryptBackupFileUsingBackupService'
|
export * from './Service/ReadAndDecryptBackupFileUsingBackupService'
|
||||||
export * from './Operations/DownloadAndDecrypt'
|
|
||||||
export * from './Operations/EncryptAndUpload'
|
|
||||||
export * from './UseCase/FileDecryptor'
|
|
||||||
export * from './UseCase/FileUploader'
|
|
||||||
export * from './UseCase/FileEncryptor'
|
|
||||||
export * from './UseCase/FileDownloader'
|
|
||||||
export * from './Types/DecryptedBytes'
|
export * from './Types/DecryptedBytes'
|
||||||
export * from './Types/EncryptedBytes'
|
export * from './Types/EncryptedBytes'
|
||||||
export * from './Types/FileDownloadProgress'
|
export * from './Types/FileDownloadProgress'
|
||||||
export * from './Types/FileUploadProgress'
|
export * from './Types/FileUploadProgress'
|
||||||
export * from './Types/FileUploadResult'
|
export * from './Types/FileUploadResult'
|
||||||
|
export * from './UseCase/FileDecryptor'
|
||||||
|
export * from './UseCase/FileDownloader'
|
||||||
|
export * from './UseCase/FileEncryptor'
|
||||||
|
export * from './UseCase/FileUploader'
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
boost: 57d2868c099736d80fcd648bf211b4431e51a558
|
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||||
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
|
||||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||||
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
|
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
|
||||||
@@ -658,7 +658,7 @@ SPEC CHECKSUMS:
|
|||||||
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
|
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
|
||||||
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
|
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
|
||||||
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
|
||||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
|
||||||
RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
|
RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
|
||||||
RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
|
RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
|
||||||
React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a
|
React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
|
||||||
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
|
||||||
import { FilesClientInterface } from '@standardnotes/files'
|
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||||
|
|
||||||
import { AlertService } from '../Alert/AlertService'
|
import { AlertService } from '../Alert/AlertService'
|
||||||
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
|
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
|
||||||
@@ -50,6 +50,7 @@ export interface ApplicationInterface {
|
|||||||
get user(): UserClientInterface
|
get user(): UserClientInterface
|
||||||
get files(): FilesClientInterface
|
get files(): FilesClientInterface
|
||||||
get subscriptions(): SubscriptionClientInterface
|
get subscriptions(): SubscriptionClientInterface
|
||||||
|
get fileBackups(): BackupServiceInterface | undefined
|
||||||
readonly identifier: ApplicationIdentifier
|
readonly identifier: ApplicationIdentifier
|
||||||
readonly platform: Platform
|
readonly platform: Platform
|
||||||
deviceInterface: DeviceInterface
|
deviceInterface: DeviceInterface
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { HistoryServiceInterface } from './../History/HistoryServiceInterface'
|
||||||
|
import { PayloadManagerInterface } from './../Payloads/PayloadManagerInterface'
|
||||||
|
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
|
||||||
|
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
|
||||||
import { StatusServiceInterface } from './../Status/StatusServiceInterface'
|
import { StatusServiceInterface } from './../Status/StatusServiceInterface'
|
||||||
import { FilesBackupService } from './BackupService'
|
import { FilesBackupService } from './BackupService'
|
||||||
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
|
||||||
@@ -20,6 +24,10 @@ describe('backup service', () => {
|
|||||||
let internalEventBus: InternalEventBusInterface
|
let internalEventBus: InternalEventBusInterface
|
||||||
let backupService: FilesBackupService
|
let backupService: FilesBackupService
|
||||||
let device: FileBackupsDevice
|
let device: FileBackupsDevice
|
||||||
|
let session: SessionsClientInterface
|
||||||
|
let storage: StorageServiceInterface
|
||||||
|
let payloads: PayloadManagerInterface
|
||||||
|
let history: HistoryServiceInterface
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
apiService = {} as jest.Mocked<ApiServiceInterface>
|
apiService = {} as jest.Mocked<ApiServiceInterface>
|
||||||
@@ -41,6 +49,8 @@ describe('backup service', () => {
|
|||||||
device.getFileBackupReadToken = jest.fn()
|
device.getFileBackupReadToken = jest.fn()
|
||||||
device.readNextChunk = jest.fn()
|
device.readNextChunk = jest.fn()
|
||||||
|
|
||||||
|
session = {} as jest.Mocked<SessionsClientInterface>
|
||||||
|
|
||||||
syncService = {} as jest.Mocked<SyncServiceInterface>
|
syncService = {} as jest.Mocked<SyncServiceInterface>
|
||||||
syncService.sync = jest.fn()
|
syncService.sync = jest.fn()
|
||||||
|
|
||||||
@@ -55,7 +65,25 @@ describe('backup service', () => {
|
|||||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||||
internalEventBus.publish = jest.fn()
|
internalEventBus.publish = jest.fn()
|
||||||
|
|
||||||
backupService = new FilesBackupService(itemManager, apiService, encryptor, device, status, crypto, internalEventBus)
|
payloads = {} as PayloadManagerInterface
|
||||||
|
history = {} as HistoryServiceInterface
|
||||||
|
|
||||||
|
storage = {} as StorageServiceInterface
|
||||||
|
storage.getValue = jest.fn().mockReturnValue('')
|
||||||
|
|
||||||
|
backupService = new FilesBackupService(
|
||||||
|
itemManager,
|
||||||
|
apiService,
|
||||||
|
encryptor,
|
||||||
|
device,
|
||||||
|
status,
|
||||||
|
crypto,
|
||||||
|
storage,
|
||||||
|
session,
|
||||||
|
payloads,
|
||||||
|
history,
|
||||||
|
internalEventBus,
|
||||||
|
)
|
||||||
|
|
||||||
crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
|
crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
|
||||||
state: {},
|
state: {},
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
|
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
|
import {
|
||||||
|
PayloadEmitSource,
|
||||||
|
FileItem,
|
||||||
|
CreateEncryptedBackupFileContextPayload,
|
||||||
|
SNNote,
|
||||||
|
SNTag,
|
||||||
|
isNote,
|
||||||
|
NoteContent,
|
||||||
|
} from '@standardnotes/models'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||||
import {
|
import {
|
||||||
FilesApiInterface,
|
FilesApiInterface,
|
||||||
@@ -10,16 +19,28 @@ import {
|
|||||||
FileBackupRecord,
|
FileBackupRecord,
|
||||||
OnChunkCallback,
|
OnChunkCallback,
|
||||||
BackupServiceInterface,
|
BackupServiceInterface,
|
||||||
|
DesktopWatchedDirectoriesChanges,
|
||||||
} from '@standardnotes/files'
|
} from '@standardnotes/files'
|
||||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
import { StatusServiceInterface } from '../Status/StatusServiceInterface'
|
import { StatusServiceInterface } from '../Status/StatusServiceInterface'
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import { log, LoggingDomain } from '../Logging'
|
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||||
|
import { StorageKey } from '../Storage/StorageKeys'
|
||||||
|
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
|
||||||
|
import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface'
|
||||||
|
import { HistoryServiceInterface } from '../History/HistoryServiceInterface'
|
||||||
|
|
||||||
|
const PlaintextBackupsDirectoryName = 'Plaintext Backups'
|
||||||
|
export const TextBackupsDirectoryName = 'Text Backups'
|
||||||
|
export const FileBackupsDirectoryName = 'File Backups'
|
||||||
|
|
||||||
export class FilesBackupService extends AbstractService implements BackupServiceInterface {
|
export class FilesBackupService extends AbstractService implements BackupServiceInterface {
|
||||||
private itemsObserverDisposer: () => void
|
private filesObserverDisposer: () => void
|
||||||
|
private notesObserverDisposer: () => void
|
||||||
|
private tagsObserverDisposer: () => void
|
||||||
|
|
||||||
private pendingFiles = new Set<string>()
|
private pendingFiles = new Set<string>()
|
||||||
private mappingCache?: FileBackupsMapping['files']
|
private mappingCache?: FileBackupsMapping['files']
|
||||||
|
|
||||||
@@ -30,45 +51,259 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
private device: FileBackupsDevice,
|
private device: FileBackupsDevice,
|
||||||
private status: StatusServiceInterface,
|
private status: StatusServiceInterface,
|
||||||
private crypto: PureCryptoInterface,
|
private crypto: PureCryptoInterface,
|
||||||
|
private storage: StorageServiceInterface,
|
||||||
|
private session: SessionsClientInterface,
|
||||||
|
private payloads: PayloadManagerInterface,
|
||||||
|
private history: HistoryServiceInterface,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
|
|
||||||
this.itemsObserverDisposer = items.addObserver<FileItem>(ContentType.File, ({ changed, inserted, source }) => {
|
this.filesObserverDisposer = items.addObserver<FileItem>(ContentType.File, ({ changed, inserted, source }) => {
|
||||||
const applicableSources = [
|
const applicableSources = [
|
||||||
PayloadEmitSource.LocalDatabaseLoaded,
|
PayloadEmitSource.LocalDatabaseLoaded,
|
||||||
PayloadEmitSource.RemoteSaved,
|
PayloadEmitSource.RemoteSaved,
|
||||||
PayloadEmitSource.RemoteRetrieved,
|
PayloadEmitSource.RemoteRetrieved,
|
||||||
]
|
]
|
||||||
|
|
||||||
if (applicableSources.includes(source)) {
|
if (applicableSources.includes(source)) {
|
||||||
void this.handleChangedFiles([...changed, ...inserted])
|
void this.handleChangedFiles([...changed, ...inserted])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const noteAndTagSources = [
|
||||||
|
PayloadEmitSource.RemoteSaved,
|
||||||
|
PayloadEmitSource.RemoteRetrieved,
|
||||||
|
PayloadEmitSource.OfflineSyncSaved,
|
||||||
|
]
|
||||||
|
|
||||||
|
this.notesObserverDisposer = items.addObserver<SNNote>(ContentType.Note, ({ changed, inserted, source }) => {
|
||||||
|
if (noteAndTagSources.includes(source)) {
|
||||||
|
void this.handleChangedNotes([...changed, ...inserted])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.tagsObserverDisposer = items.addObserver<SNTag>(ContentType.Tag, ({ changed, inserted, source }) => {
|
||||||
|
if (noteAndTagSources.includes(source)) {
|
||||||
|
void this.handleChangedTags([...changed, ...inserted])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async importWatchedDirectoryChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {
|
||||||
|
for (const change of changes) {
|
||||||
|
const existingItem = this.items.findItem(change.itemUuid)
|
||||||
|
if (!existingItem) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNote(existingItem)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContent: NoteContent = {
|
||||||
|
...existingItem.payload.content,
|
||||||
|
preview_html: undefined,
|
||||||
|
preview_plain: undefined,
|
||||||
|
text: change.content,
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadCopy = existingItem.payload.copy({
|
||||||
|
content: newContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.payloads.importPayloads([payloadCopy], this.history.getHistoryMapCopy())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
this.itemsObserverDisposer()
|
this.filesObserverDisposer()
|
||||||
|
this.notesObserverDisposer()
|
||||||
|
this.tagsObserverDisposer()
|
||||||
;(this.items as unknown) = undefined
|
;(this.items as unknown) = undefined
|
||||||
;(this.api as unknown) = undefined
|
;(this.api as unknown) = undefined
|
||||||
;(this.encryptor as unknown) = undefined
|
;(this.encryptor as unknown) = undefined
|
||||||
;(this.device as unknown) = undefined
|
;(this.device as unknown) = undefined
|
||||||
;(this.status as unknown) = undefined
|
;(this.status as unknown) = undefined
|
||||||
;(this.crypto as unknown) = undefined
|
;(this.crypto as unknown) = undefined
|
||||||
|
;(this.storage as unknown) = undefined
|
||||||
|
;(this.session as unknown) = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
public isFilesBackupsEnabled(): Promise<boolean> {
|
override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
|
||||||
return this.device.isFilesBackupsEnabled()
|
if (stage === ApplicationStage.Launched_10) {
|
||||||
|
void this.automaticallyEnableTextBackupsIfPreferenceNotSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async automaticallyEnableTextBackupsIfPreferenceNotSet(): Promise<void> {
|
||||||
|
if (this.storage.getValue(StorageKey.TextBackupsEnabled) == undefined) {
|
||||||
|
this.storage.setValue(StorageKey.TextBackupsEnabled, true)
|
||||||
|
const location = `${await this.device.getUserDocumentsDirectory()}/${this.prependWorkspacePathForPath(
|
||||||
|
TextBackupsDirectoryName,
|
||||||
|
)}`
|
||||||
|
this.storage.setValue(StorageKey.TextBackupsLocation, location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openAllDirectoriesContainingBackupFiles(): void {
|
||||||
|
const fileBackupsLocation = this.getFilesBackupsLocation()
|
||||||
|
const plaintextBackupsLocation = this.getPlaintextBackupsLocation()
|
||||||
|
const textBackupsLocation = this.getTextBackupsLocation()
|
||||||
|
|
||||||
|
if (fileBackupsLocation) {
|
||||||
|
void this.device.openLocation(fileBackupsLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintextBackupsLocation) {
|
||||||
|
void this.device.openLocation(plaintextBackupsLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textBackupsLocation) {
|
||||||
|
void this.device.openLocation(textBackupsLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isFilesBackupsEnabled(): boolean {
|
||||||
|
return this.storage.getValue(StorageKey.FileBackupsEnabled, undefined, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilesBackupsLocation(): string | undefined {
|
||||||
|
return this.storage.getValue(StorageKey.FileBackupsLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
isTextBackupsEnabled(): boolean {
|
||||||
|
return this.storage.getValue(StorageKey.TextBackupsEnabled, undefined, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
prependWorkspacePathForPath(path: string): string {
|
||||||
|
const workspacePath = this.session.getWorkspaceDisplayIdentifier()
|
||||||
|
|
||||||
|
return `${workspacePath}/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async enableTextBackups(): Promise<void> {
|
||||||
|
let location = this.getTextBackupsLocation()
|
||||||
|
if (!location) {
|
||||||
|
location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
|
this.prependWorkspacePathForPath(TextBackupsDirectoryName),
|
||||||
|
)
|
||||||
|
if (!location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage.setValue(StorageKey.TextBackupsEnabled, true)
|
||||||
|
this.storage.setValue(StorageKey.TextBackupsLocation, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
disableTextBackups(): void {
|
||||||
|
this.storage.setValue(StorageKey.TextBackupsEnabled, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextBackupsLocation(): string | undefined {
|
||||||
|
return this.storage.getValue(StorageKey.TextBackupsLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
async openTextBackupsLocation(): Promise<void> {
|
||||||
|
const location = this.getTextBackupsLocation()
|
||||||
|
if (location) {
|
||||||
|
void this.device.openLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeTextBackupsLocation(): Promise<string | undefined> {
|
||||||
|
const oldLocation = this.getTextBackupsLocation()
|
||||||
|
const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
|
this.prependWorkspacePathForPath(TextBackupsDirectoryName),
|
||||||
|
oldLocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!newLocation) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage.setValue(StorageKey.TextBackupsLocation, newLocation)
|
||||||
|
|
||||||
|
return newLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTextBackupData(data: string): Promise<void> {
|
||||||
|
const location = this.getTextBackupsLocation()
|
||||||
|
if (!location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.device.saveTextBackupData(location, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaintextBackupsEnabled(): boolean {
|
||||||
|
return this.storage.getValue(StorageKey.PlaintextBackupsEnabled, undefined, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enablePlaintextBackups(): Promise<void> {
|
||||||
|
let location = this.getPlaintextBackupsLocation()
|
||||||
|
if (!location) {
|
||||||
|
location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
|
this.prependWorkspacePathForPath(PlaintextBackupsDirectoryName),
|
||||||
|
)
|
||||||
|
if (!location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage.setValue(StorageKey.PlaintextBackupsEnabled, true)
|
||||||
|
this.storage.setValue(StorageKey.PlaintextBackupsLocation, location)
|
||||||
|
|
||||||
|
void this.handleChangedNotes(this.items.getItems<SNNote>(ContentType.Note))
|
||||||
|
}
|
||||||
|
|
||||||
|
disablePlaintextBackups(): void {
|
||||||
|
this.storage.setValue(StorageKey.PlaintextBackupsEnabled, false)
|
||||||
|
this.storage.setValue(StorageKey.PlaintextBackupsLocation, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaintextBackupsLocation(): string | undefined {
|
||||||
|
return this.storage.getValue(StorageKey.PlaintextBackupsLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
async openPlaintextBackupsLocation(): Promise<void> {
|
||||||
|
const location = this.getPlaintextBackupsLocation()
|
||||||
|
if (location) {
|
||||||
|
void this.device.openLocation(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePlaintextBackupsLocation(): Promise<string | undefined> {
|
||||||
|
const oldLocation = this.getPlaintextBackupsLocation()
|
||||||
|
const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
|
this.prependWorkspacePathForPath(PlaintextBackupsDirectoryName),
|
||||||
|
oldLocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!newLocation) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage.setValue(StorageKey.PlaintextBackupsLocation, newLocation)
|
||||||
|
|
||||||
|
return newLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enableFilesBackups(): Promise<void> {
|
public async enableFilesBackups(): Promise<void> {
|
||||||
await this.device.enableFilesBackups()
|
let location = this.getFilesBackupsLocation()
|
||||||
|
if (!location) {
|
||||||
if (!(await this.isFilesBackupsEnabled())) {
|
location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
return
|
this.prependWorkspacePathForPath(FileBackupsDirectoryName),
|
||||||
|
)
|
||||||
|
if (!location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.storage.setValue(StorageKey.FileBackupsEnabled, true)
|
||||||
|
this.storage.setValue(StorageKey.FileBackupsLocation, location)
|
||||||
|
|
||||||
this.backupAllFiles()
|
this.backupAllFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,24 +313,39 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
void this.handleChangedFiles(files)
|
void this.handleChangedFiles(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
public disableFilesBackups(): Promise<void> {
|
public disableFilesBackups(): void {
|
||||||
return this.device.disableFilesBackups()
|
this.storage.setValue(StorageKey.FileBackupsEnabled, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
public changeFilesBackupsLocation(): Promise<string | undefined> {
|
public async changeFilesBackupsLocation(): Promise<string | undefined> {
|
||||||
return this.device.changeFilesBackupsLocation()
|
const oldLocation = this.getFilesBackupsLocation()
|
||||||
|
const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||||
|
this.prependWorkspacePathForPath(FileBackupsDirectoryName),
|
||||||
|
oldLocation,
|
||||||
|
)
|
||||||
|
if (!newLocation) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storage.setValue(StorageKey.FileBackupsLocation, newLocation)
|
||||||
|
|
||||||
|
return newLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFilesBackupsLocation(): Promise<string> {
|
public async openFilesBackupsLocation(): Promise<void> {
|
||||||
return this.device.getFilesBackupsLocation()
|
const location = this.getFilesBackupsLocation()
|
||||||
|
if (location) {
|
||||||
|
void this.device.openLocation(location)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openFilesBackupsLocation(): Promise<void> {
|
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files'] | undefined> {
|
||||||
return this.device.openFilesBackupsLocation()
|
const location = this.getFilesBackupsLocation()
|
||||||
}
|
if (!location) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files']> {
|
const result = (await this.device.getFilesBackupsMappingFile(location)).files
|
||||||
const result = (await this.device.getFilesBackupsMappingFile()).files
|
|
||||||
|
|
||||||
this.mappingCache = result
|
this.mappingCache = result
|
||||||
|
|
||||||
@@ -106,30 +356,39 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
this.mappingCache = undefined
|
this.mappingCache = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files']> {
|
private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files'] | undefined> {
|
||||||
return this.mappingCache ?? (await this.getBackupsMappingFromDisk())
|
return this.mappingCache ?? (await this.getBackupsMappingFromDisk())
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined> {
|
public async getFileBackupInfo(file: { uuid: string }): Promise<FileBackupRecord | undefined> {
|
||||||
const mapping = await this.getBackupsMappingFromCache()
|
const mapping = await this.getBackupsMappingFromCache()
|
||||||
|
if (!mapping) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const record = mapping[file.uuid]
|
const record = mapping[file.uuid]
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFileBackupAbsolutePath(record: FileBackupRecord): string {
|
||||||
|
const location = this.getFilesBackupsLocation()
|
||||||
|
return `${location}/${record.relativePath}`
|
||||||
|
}
|
||||||
|
|
||||||
public async openFileBackup(record: FileBackupRecord): Promise<void> {
|
public async openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||||
await this.device.openFileBackup(record)
|
const location = this.getFileBackupAbsolutePath(record)
|
||||||
|
await this.device.openLocation(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleChangedFiles(files: FileItem[]): Promise<void> {
|
private async handleChangedFiles(files: FileItem[]): Promise<void> {
|
||||||
if (files.length === 0) {
|
if (files.length === 0 || !this.isFilesBackupsEnabled()) {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await this.isFilesBackupsEnabled())) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping = await this.getBackupsMappingFromDisk()
|
const mapping = await this.getBackupsMappingFromDisk()
|
||||||
|
if (!mapping) {
|
||||||
|
throw new ClientDisplayableError('No backups mapping found')
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (this.pendingFiles.has(file.uuid)) {
|
if (this.pendingFiles.has(file.uuid)) {
|
||||||
@@ -150,6 +409,36 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
this.invalidateMappingCache()
|
this.invalidateMappingCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleChangedNotes(notes: SNNote[]): Promise<void> {
|
||||||
|
if (notes.length === 0 || !this.isPlaintextBackupsEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = this.getPlaintextBackupsLocation()
|
||||||
|
if (!location) {
|
||||||
|
throw new ClientDisplayableError('No plaintext backups location found')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
const tags = this.items.getSortedTagsForItem(note)
|
||||||
|
const tagNames = tags.map((tag) => this.items.getTagLongTitle(tag))
|
||||||
|
await this.device.savePlaintextNoteBackup(location, note.uuid, note.title, tagNames, note.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.device.persistPlaintextBackupsMappingFile(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleChangedTags(tags: SNTag[]): Promise<void> {
|
||||||
|
if (tags.length === 0 || !this.isPlaintextBackupsEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
const notes = this.items.referencesForItem<SNNote>(tag, ContentType.Note)
|
||||||
|
await this.handleChangedNotes(notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'> {
|
async readEncryptedFileFromBackup(uuid: string, onChunk: OnChunkCallback): Promise<'success' | 'failed' | 'aborted'> {
|
||||||
const fileBackup = await this.getFileBackupInfo({ uuid })
|
const fileBackup = await this.getFileBackupInfo({ uuid })
|
||||||
|
|
||||||
@@ -157,7 +446,8 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
return 'failed'
|
return 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await this.device.getFileBackupReadToken(fileBackup)
|
const path = `${this.getFilesBackupsLocation()}/${fileBackup.relativePath}/${fileBackup.binaryFileName}`
|
||||||
|
const token = await this.device.getFileBackupReadToken(path)
|
||||||
|
|
||||||
let readMore = true
|
let readMore = true
|
||||||
let index = 0
|
let index = 0
|
||||||
@@ -176,7 +466,10 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
|
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
|
||||||
log(LoggingDomain.FilesBackups, 'Backing up file locally', file.uuid)
|
const location = this.getFilesBackupsLocation()
|
||||||
|
if (!location) {
|
||||||
|
return 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
const messageId = this.status.addMessage(`Backing up file ${file.name}...`)
|
const messageId = this.status.addMessage(`Backing up file ${file.name}...`)
|
||||||
|
|
||||||
@@ -189,6 +482,7 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
const itemsKey = this.items.getDisplayableItemsKeys().find((k) => k.uuid === encryptedFile.items_key_id)
|
const itemsKey = this.items.getDisplayableItemsKeys().find((k) => k.uuid === encryptedFile.items_key_id)
|
||||||
|
|
||||||
if (!itemsKey) {
|
if (!itemsKey) {
|
||||||
|
this.status.removeMessage(messageId)
|
||||||
return 'failed'
|
return 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +495,7 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
const token = await this.api.createFileValetToken(file.remoteIdentifier, 'read')
|
const token = await this.api.createFileValetToken(file.remoteIdentifier, 'read')
|
||||||
|
|
||||||
if (token instanceof ClientDisplayableError) {
|
if (token instanceof ClientDisplayableError) {
|
||||||
|
this.status.removeMessage(messageId)
|
||||||
return 'failed'
|
return 'failed'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +513,7 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
|
|
||||||
const metaFileAsString = JSON.stringify(metaFile, null, 2)
|
const metaFileAsString = JSON.stringify(metaFile, null, 2)
|
||||||
|
|
||||||
const result = await this.device.saveFilesBackupsFile(file.uuid, metaFileAsString, {
|
const result = await this.device.saveFilesBackupsFile(location, file.uuid, metaFileAsString, {
|
||||||
chunkSizes: file.encryptedChunkSizes,
|
chunkSizes: file.encryptedChunkSizes,
|
||||||
url: this.api.getFilesDownloadUrl(),
|
url: this.api.getFilesDownloadUrl(),
|
||||||
valetToken: token,
|
valetToken: token,
|
||||||
@@ -235,4 +530,18 @@ export class FilesBackupService extends AbstractService implements BackupService
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not presently used or enabled. It works, but presently has the following edge cases:
|
||||||
|
* 1. Editing the note directly in SN triggers an immediate backup which triggers a file change which triggers the observer
|
||||||
|
* 2. Since changes are based on filenames, a note with the same title as another may not properly map to the correct uuid
|
||||||
|
* 3. Opening the file triggers a watch event from Node's watch API.
|
||||||
|
* 4. Gives web code ability to monitor arbitrary locations. Needs whitelisting mechanism.
|
||||||
|
*/
|
||||||
|
disabledExperimental_monitorPlaintextBackups(): void {
|
||||||
|
const location = this.getPlaintextBackupsLocation()
|
||||||
|
if (location) {
|
||||||
|
void this.device.monitorPlaintextBackupsLocationForChanges(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
import { DecryptedTransferPayload } from '@standardnotes/models'
|
import { DecryptedTransferPayload } from '@standardnotes/models'
|
||||||
import { FileBackupsDevice } from '@standardnotes/files'
|
import { DesktopWatchedDirectoriesChanges, FileBackupsDevice } from '@standardnotes/files'
|
||||||
|
|
||||||
export interface WebClientRequiresDesktopMethods extends FileBackupsDevice {
|
export interface WebClientRequiresDesktopMethods extends FileBackupsDevice {
|
||||||
localBackupsCount(): Promise<number>
|
|
||||||
|
|
||||||
viewlocalBackups(): void
|
|
||||||
|
|
||||||
deleteLocalBackups(): Promise<void>
|
|
||||||
|
|
||||||
syncComponents(payloads: unknown[]): void
|
syncComponents(payloads: unknown[]): void
|
||||||
|
|
||||||
onMajorDataChange(): void
|
|
||||||
|
|
||||||
onInitialDataLoad(): void
|
|
||||||
|
|
||||||
onSearch(text?: string): void
|
onSearch(text?: string): void
|
||||||
|
|
||||||
downloadBackup(): void | Promise<void>
|
|
||||||
|
|
||||||
get extensionsServerHost(): string
|
get extensionsServerHost(): string
|
||||||
|
|
||||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
|
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean>
|
||||||
@@ -32,9 +20,5 @@ export interface DesktopClientRequiresWebMethods {
|
|||||||
|
|
||||||
onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise<void>
|
onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise<void>
|
||||||
|
|
||||||
requestBackupFile(): Promise<string | undefined>
|
handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void>
|
||||||
|
|
||||||
didBeginBackup(): void
|
|
||||||
|
|
||||||
didFinishBackup(success: boolean): void
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { HistoryMap } from '@standardnotes/models'
|
||||||
|
|
||||||
|
export interface HistoryServiceInterface {
|
||||||
|
getHistoryMapCopy(): HistoryMap
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ItemContent,
|
ItemContent,
|
||||||
PredicateInterface,
|
PredicateInterface,
|
||||||
DecryptedPayload,
|
DecryptedPayload,
|
||||||
|
SNTag,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
|
|
||||||
@@ -96,4 +97,11 @@ export interface ItemManagerInterface extends AbstractService {
|
|||||||
subItemsMatchingPredicates<T extends DecryptedItemInterface>(items: T[], predicates: PredicateInterface<T>[]): T[]
|
subItemsMatchingPredicates<T extends DecryptedItemInterface>(items: T[], predicates: PredicateInterface<T>[]): T[]
|
||||||
removeAllItemsFromMemory(): Promise<void>
|
removeAllItemsFromMemory(): Promise<void>
|
||||||
getDirtyItems(): (DecryptedItemInterface | DeletedItemInterface)[]
|
getDirtyItems(): (DecryptedItemInterface | DeletedItemInterface)[]
|
||||||
|
getTagLongTitle(tag: SNTag): string
|
||||||
|
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
|
||||||
|
referencesForItem<I extends DecryptedItemInterface = DecryptedItemInterface>(
|
||||||
|
itemToLookupUuidFor: DecryptedItemInterface,
|
||||||
|
contentType?: ContentType,
|
||||||
|
): I[]
|
||||||
|
findItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import {
|
|||||||
EncryptedPayloadInterface,
|
EncryptedPayloadInterface,
|
||||||
FullyFormedPayloadInterface,
|
FullyFormedPayloadInterface,
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
|
DecryptedPayloadInterface,
|
||||||
|
HistoryMap,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { IntegrityPayload } from '@standardnotes/responses'
|
import { IntegrityPayload } from '@standardnotes/responses'
|
||||||
|
|
||||||
@@ -21,4 +23,6 @@ export interface PayloadManagerInterface {
|
|||||||
* Returns a detached array of all items which are not deleted
|
* Returns a detached array of all items which are not deleted
|
||||||
*/
|
*/
|
||||||
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
||||||
|
|
||||||
|
importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Base64String } from '@standardnotes/sncrypto-common'
|
|||||||
import { SessionManagerResponse } from './SessionManagerResponse'
|
import { SessionManagerResponse } from './SessionManagerResponse'
|
||||||
|
|
||||||
export interface SessionsClientInterface {
|
export interface SessionsClientInterface {
|
||||||
|
getWorkspaceDisplayIdentifier(): string
|
||||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||||
getUser(): User | undefined
|
getUser(): User | undefined
|
||||||
isCurrentSessionReadOnly(): boolean | undefined
|
isCurrentSessionReadOnly(): boolean | undefined
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export enum StorageKey {
|
|||||||
LaunchPriorityUuids = 'launch_priority_uuids',
|
LaunchPriorityUuids = 'launch_priority_uuids',
|
||||||
LastReadChangelogVersion = 'last_read_changelog_version',
|
LastReadChangelogVersion = 'last_read_changelog_version',
|
||||||
MomentsEnabled = 'moments_enabled',
|
MomentsEnabled = 'moments_enabled',
|
||||||
|
TextBackupsEnabled = 'text_backups_enabled',
|
||||||
|
TextBackupsLocation = 'text_backups_location',
|
||||||
|
PlaintextBackupsEnabled = 'plaintext_backups_enabled',
|
||||||
|
PlaintextBackupsLocation = 'plaintext_backups_location',
|
||||||
|
FileBackupsEnabled = 'file_backups_enabled',
|
||||||
|
FileBackupsLocation = 'file_backups_location',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NonwrappedStorageKey {
|
export enum NonwrappedStorageKey {
|
||||||
|
|||||||
@@ -4,19 +4,21 @@ export * from './Application/AppGroupManagedApplication'
|
|||||||
export * from './Application/ApplicationInterface'
|
export * from './Application/ApplicationInterface'
|
||||||
export * from './Application/ApplicationStage'
|
export * from './Application/ApplicationStage'
|
||||||
export * from './Application/DeinitCallback'
|
export * from './Application/DeinitCallback'
|
||||||
export * from './Application/DeinitSource'
|
|
||||||
export * from './Application/DeinitMode'
|
export * from './Application/DeinitMode'
|
||||||
|
export * from './Application/DeinitSource'
|
||||||
|
export * from './Application/WebApplicationInterface'
|
||||||
export * from './Auth/AuthClientInterface'
|
export * from './Auth/AuthClientInterface'
|
||||||
export * from './Auth/AuthManager'
|
export * from './Auth/AuthManager'
|
||||||
export * from './Authenticator/AuthenticatorClientInterface'
|
export * from './Authenticator/AuthenticatorClientInterface'
|
||||||
export * from './Authenticator/AuthenticatorManager'
|
export * from './Authenticator/AuthenticatorManager'
|
||||||
export * from './User/UserClientInterface'
|
|
||||||
export * from './Application/WebApplicationInterface'
|
|
||||||
export * from './Backups/BackupService'
|
export * from './Backups/BackupService'
|
||||||
export * from './Challenge'
|
export * from './Challenge'
|
||||||
export * from './Component/ComponentManagerInterface'
|
export * from './Component/ComponentManagerInterface'
|
||||||
export * from './Component/ComponentViewerError'
|
export * from './Component/ComponentViewerError'
|
||||||
export * from './Component/ComponentViewerInterface'
|
export * from './Component/ComponentViewerInterface'
|
||||||
|
export * from './Device/DatabaseItemMetadata'
|
||||||
|
export * from './Device/DatabaseLoadOptions'
|
||||||
|
export * from './Device/DatabaseLoadSorter'
|
||||||
export * from './Device/DesktopDeviceInterface'
|
export * from './Device/DesktopDeviceInterface'
|
||||||
export * from './Device/DesktopManagerInterface'
|
export * from './Device/DesktopManagerInterface'
|
||||||
export * from './Device/DesktopWebCommunication'
|
export * from './Device/DesktopWebCommunication'
|
||||||
@@ -24,9 +26,6 @@ export * from './Device/DeviceInterface'
|
|||||||
export * from './Device/MobileDeviceInterface'
|
export * from './Device/MobileDeviceInterface'
|
||||||
export * from './Device/TypeCheck'
|
export * from './Device/TypeCheck'
|
||||||
export * from './Device/WebOrDesktopDeviceInterface'
|
export * from './Device/WebOrDesktopDeviceInterface'
|
||||||
export * from './Device/DatabaseLoadOptions'
|
|
||||||
export * from './Device/DatabaseItemMetadata'
|
|
||||||
export * from './Device/DatabaseLoadSorter'
|
|
||||||
export * from './Diagnostics/ServiceDiagnostics'
|
export * from './Diagnostics/ServiceDiagnostics'
|
||||||
export * from './Encryption/BackupFileDecryptor'
|
export * from './Encryption/BackupFileDecryptor'
|
||||||
export * from './Encryption/EncryptionService'
|
export * from './Encryption/EncryptionService'
|
||||||
@@ -40,12 +39,13 @@ export * from './Event/EventObserver'
|
|||||||
export * from './Event/SyncEvent'
|
export * from './Event/SyncEvent'
|
||||||
export * from './Event/SyncEventReceiver'
|
export * from './Event/SyncEventReceiver'
|
||||||
export * from './Event/WebAppEvent'
|
export * from './Event/WebAppEvent'
|
||||||
export * from './Feature/FeatureStatus'
|
|
||||||
export * from './Feature/FeaturesClientInterface'
|
export * from './Feature/FeaturesClientInterface'
|
||||||
export * from './Feature/FeaturesEvent'
|
export * from './Feature/FeaturesEvent'
|
||||||
|
export * from './Feature/FeatureStatus'
|
||||||
export * from './Feature/OfflineSubscriptionEntitlements'
|
export * from './Feature/OfflineSubscriptionEntitlements'
|
||||||
export * from './Feature/SetOfflineFeaturesFunctionResponse'
|
export * from './Feature/SetOfflineFeaturesFunctionResponse'
|
||||||
export * from './Files/FileService'
|
export * from './Files/FileService'
|
||||||
|
export * from './History/HistoryServiceInterface'
|
||||||
export * from './Integrity/IntegrityApiInterface'
|
export * from './Integrity/IntegrityApiInterface'
|
||||||
export * from './Integrity/IntegrityEvent'
|
export * from './Integrity/IntegrityEvent'
|
||||||
export * from './Integrity/IntegrityEventPayload'
|
export * from './Integrity/IntegrityEventPayload'
|
||||||
@@ -59,9 +59,9 @@ export * from './Internal/InternalEventType'
|
|||||||
export * from './Item/ItemCounter'
|
export * from './Item/ItemCounter'
|
||||||
export * from './Item/ItemCounterInterface'
|
export * from './Item/ItemCounterInterface'
|
||||||
export * from './Item/ItemManagerInterface'
|
export * from './Item/ItemManagerInterface'
|
||||||
|
export * from './Item/ItemRelationshipDirection'
|
||||||
export * from './Item/ItemsClientInterface'
|
export * from './Item/ItemsClientInterface'
|
||||||
export * from './Item/ItemsServerInterface'
|
export * from './Item/ItemsServerInterface'
|
||||||
export * from './Item/ItemRelationshipDirection'
|
|
||||||
export * from './Mutator/MutatorClientInterface'
|
export * from './Mutator/MutatorClientInterface'
|
||||||
export * from './Payloads/PayloadManagerInterface'
|
export * from './Payloads/PayloadManagerInterface'
|
||||||
export * from './Preferences/PreferenceServiceInterface'
|
export * from './Preferences/PreferenceServiceInterface'
|
||||||
@@ -76,21 +76,22 @@ export * from './Session/SessionManagerResponse'
|
|||||||
export * from './Session/SessionsClientInterface'
|
export * from './Session/SessionsClientInterface'
|
||||||
export * from './Status/StatusService'
|
export * from './Status/StatusService'
|
||||||
export * from './Status/StatusServiceInterface'
|
export * from './Status/StatusServiceInterface'
|
||||||
export * from './Storage/StorageKeys'
|
|
||||||
export * from './Storage/InMemoryStore'
|
export * from './Storage/InMemoryStore'
|
||||||
export * from './Storage/KeyValueStoreInterface'
|
export * from './Storage/KeyValueStoreInterface'
|
||||||
|
export * from './Storage/StorageKeys'
|
||||||
export * from './Storage/StorageServiceInterface'
|
export * from './Storage/StorageServiceInterface'
|
||||||
export * from './Storage/StorageTypes'
|
export * from './Storage/StorageTypes'
|
||||||
export * from './Strings/InfoStrings'
|
export * from './Strings/InfoStrings'
|
||||||
export * from './Strings/Messages'
|
export * from './Strings/Messages'
|
||||||
export * from './Subscription/SubscriptionClientInterface'
|
|
||||||
export * from './Subscription/SubscriptionManager'
|
|
||||||
export * from './Subscription/AppleIAPProductId'
|
export * from './Subscription/AppleIAPProductId'
|
||||||
export * from './Subscription/AppleIAPReceipt'
|
export * from './Subscription/AppleIAPReceipt'
|
||||||
|
export * from './Subscription/SubscriptionClientInterface'
|
||||||
|
export * from './Subscription/SubscriptionManager'
|
||||||
export * from './Sync/SyncMode'
|
export * from './Sync/SyncMode'
|
||||||
export * from './Sync/SyncOptions'
|
export * from './Sync/SyncOptions'
|
||||||
export * from './Sync/SyncQueueStrategy'
|
export * from './Sync/SyncQueueStrategy'
|
||||||
export * from './Sync/SyncServiceInterface'
|
export * from './Sync/SyncServiceInterface'
|
||||||
export * from './Sync/SyncSource'
|
export * from './Sync/SyncSource'
|
||||||
export * from './User/UserClientInterface'
|
export * from './User/UserClientInterface'
|
||||||
|
export * from './User/UserClientInterface'
|
||||||
export * from './User/UserService'
|
export * from './User/UserService'
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
private declare _getRevision: GetRevision
|
private declare _getRevision: GetRevision
|
||||||
private declare _deleteRevision: DeleteRevision
|
private declare _deleteRevision: DeleteRevision
|
||||||
|
|
||||||
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
public internalEventBus!: ExternalServices.InternalEventBusInterface
|
||||||
|
|
||||||
private eventHandlers: ApplicationObserver[] = []
|
private eventHandlers: ApplicationObserver[] = []
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -1184,13 +1184,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.createSettingsService()
|
this.createSettingsService()
|
||||||
this.createFeaturesService()
|
this.createFeaturesService()
|
||||||
this.createComponentManager()
|
this.createComponentManager()
|
||||||
this.createMigrationService()
|
|
||||||
this.createMfaService()
|
this.createMfaService()
|
||||||
|
|
||||||
this.createStatusService()
|
this.createStatusService()
|
||||||
if (isDesktopDevice(this.deviceInterface)) {
|
if (isDesktopDevice(this.deviceInterface)) {
|
||||||
this.createFilesBackupService(this.deviceInterface)
|
this.createFilesBackupService(this.deviceInterface)
|
||||||
}
|
}
|
||||||
|
this.createMigrationService()
|
||||||
this.createFileService()
|
this.createFileService()
|
||||||
|
|
||||||
this.createIntegrityService()
|
this.createIntegrityService()
|
||||||
@@ -1381,6 +1381,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
identifier: this.identifier,
|
identifier: this.identifier,
|
||||||
internalEventBus: this.internalEventBus,
|
internalEventBus: this.internalEventBus,
|
||||||
legacySessionStorageMapper: this.legacySessionStorageMapper,
|
legacySessionStorageMapper: this.legacySessionStorageMapper,
|
||||||
|
backups: this.fileBackups,
|
||||||
})
|
})
|
||||||
this.services.push(this.migrationService)
|
this.services.push(this.migrationService)
|
||||||
}
|
}
|
||||||
@@ -1584,6 +1585,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.httpService,
|
this.httpService,
|
||||||
this.sessionStorageMapper,
|
this.sessionStorageMapper,
|
||||||
this.legacySessionStorageMapper,
|
this.legacySessionStorageMapper,
|
||||||
|
this.identifier,
|
||||||
this.internalEventBus,
|
this.internalEventBus,
|
||||||
)
|
)
|
||||||
this.serviceObservers.push(
|
this.serviceObservers.push(
|
||||||
@@ -1761,6 +1763,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
device,
|
device,
|
||||||
this.statusService,
|
this.statusService,
|
||||||
this.options.crypto,
|
this.options.crypto,
|
||||||
|
this.storage,
|
||||||
|
this.sessions,
|
||||||
|
this.payloadManager,
|
||||||
|
this.historyManager,
|
||||||
this.internalEventBus,
|
this.internalEventBus,
|
||||||
)
|
)
|
||||||
this.services.push(this.filesBackupService)
|
this.services.push(this.filesBackupService)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { BackupServiceInterface } from '@standardnotes/files'
|
||||||
import { Environment } from '@standardnotes/models'
|
import { Environment } from '@standardnotes/models'
|
||||||
import { DeviceInterface, InternalEventBusInterface, EncryptionService } from '@standardnotes/services'
|
import { DeviceInterface, InternalEventBusInterface, EncryptionService } from '@standardnotes/services'
|
||||||
|
|
||||||
import { SNSessionManager } from '../Services/Session/SessionManager'
|
import { SNSessionManager } from '../Services/Session/SessionManager'
|
||||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||||
@@ -13,6 +13,7 @@ export type MigrationServices = {
|
|||||||
storageService: DiskStorageService
|
storageService: DiskStorageService
|
||||||
challengeService: ChallengeService
|
challengeService: ChallengeService
|
||||||
sessionManager: SNSessionManager
|
sessionManager: SNSessionManager
|
||||||
|
backups?: BackupServiceInterface
|
||||||
itemManager: ItemManager
|
itemManager: ItemManager
|
||||||
singletonManager: SNSingletonManager
|
singletonManager: SNSingletonManager
|
||||||
featuresService: SNFeaturesService
|
featuresService: SNFeaturesService
|
||||||
|
|||||||
51
packages/snjs/lib/Migrations/Versions/2_167_6.ts
Normal file
51
packages/snjs/lib/Migrations/Versions/2_167_6.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
ApplicationStage,
|
||||||
|
FileBackupsDirectoryName,
|
||||||
|
StorageKey,
|
||||||
|
TextBackupsDirectoryName,
|
||||||
|
isDesktopDevice,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
import { Migration } from '@Lib/Migrations/Migration'
|
||||||
|
|
||||||
|
export class Migration2_167_6 extends Migration {
|
||||||
|
static override version(): string {
|
||||||
|
return '2.167.6'
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerStageHandlers(): void {
|
||||||
|
this.registerStageHandler(ApplicationStage.Launched_10, async () => {
|
||||||
|
await this.migrateStorageKeysForDesktopBackups()
|
||||||
|
this.markDone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async migrateStorageKeysForDesktopBackups(): Promise<void> {
|
||||||
|
const device = this.services.deviceInterface
|
||||||
|
if (!isDesktopDevice(device) || !this.services.backups) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileBackupsEnabled = await device.isLegacyFilesBackupsEnabled()
|
||||||
|
this.services.storageService.setValue(StorageKey.FileBackupsEnabled, fileBackupsEnabled)
|
||||||
|
|
||||||
|
if (fileBackupsEnabled) {
|
||||||
|
const legacyLocation = await device.getLegacyFilesBackupsLocation()
|
||||||
|
const newLocation = `${legacyLocation}/${this.services.backups.prependWorkspacePathForPath(
|
||||||
|
FileBackupsDirectoryName,
|
||||||
|
)}`
|
||||||
|
await device.migrateLegacyFileBackupsToNewStructure(newLocation)
|
||||||
|
this.services.storageService.setValue(StorageKey.FileBackupsLocation, newLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasLegacyDisabled = await device.wasLegacyTextBackupsExplicitlyDisabled()
|
||||||
|
if (wasLegacyDisabled) {
|
||||||
|
this.services.storageService.setValue(StorageKey.TextBackupsEnabled, false)
|
||||||
|
} else {
|
||||||
|
const newTextBackupsLocation = `${await device.getLegacyTextBackupsLocation()}/${this.services.backups.prependWorkspacePathForPath(
|
||||||
|
TextBackupsDirectoryName,
|
||||||
|
)}`
|
||||||
|
this.services.storageService.setValue(StorageKey.TextBackupsLocation, newTextBackupsLocation)
|
||||||
|
this.services.storageService.setValue(StorageKey.TextBackupsEnabled, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/snjs/lib/Migrations/Versions/README.md
Normal file
5
packages/snjs/lib/Migrations/Versions/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## To create a migration:
|
||||||
|
|
||||||
|
1. Create a new file inside versions specifiying the would-be version of SNJS that would result when publishing your migration. For example, if the current SNJS version is 1.0.0 in package.json, your migration version should be 1.0.1 to target users below this version.
|
||||||
|
|
||||||
|
2. **Important** Export your migration inside the index.ts file.
|
||||||
@@ -3,7 +3,15 @@ import { Migration2_7_0 } from './2_7_0'
|
|||||||
import { Migration2_20_0 } from './2_20_0'
|
import { Migration2_20_0 } from './2_20_0'
|
||||||
import { Migration2_36_0 } from './2_36_0'
|
import { Migration2_36_0 } from './2_36_0'
|
||||||
import { Migration2_42_0 } from './2_42_0'
|
import { Migration2_42_0 } from './2_42_0'
|
||||||
|
import { Migration2_167_6 } from './2_167_6'
|
||||||
|
|
||||||
export const MigrationClasses = [Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0]
|
export const MigrationClasses = [
|
||||||
|
Migration2_0_15,
|
||||||
|
Migration2_7_0,
|
||||||
|
Migration2_20_0,
|
||||||
|
Migration2_36_0,
|
||||||
|
Migration2_42_0,
|
||||||
|
Migration2_167_6,
|
||||||
|
]
|
||||||
|
|
||||||
export { Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0 }
|
export { Migration2_0_15, Migration2_7_0, Migration2_20_0, Migration2_36_0, Migration2_42_0, Migration2_167_6 }
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { DiskStorageService } from '@Lib/Services/Storage/DiskStorageService'
|
|||||||
import { UuidString } from '../../Types/UuidString'
|
import { UuidString } from '../../Types/UuidString'
|
||||||
import * as Models from '@standardnotes/models'
|
import * as Models from '@standardnotes/models'
|
||||||
import { SNNote } from '@standardnotes/models'
|
import { SNNote } from '@standardnotes/models'
|
||||||
import { AbstractService, DeviceInterface, InternalEventBusInterface } from '@standardnotes/services'
|
import {
|
||||||
|
AbstractService,
|
||||||
|
DeviceInterface,
|
||||||
|
HistoryServiceInterface,
|
||||||
|
InternalEventBusInterface,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
|
||||||
/** The amount of revisions per item above which should call for an optimization. */
|
/** The amount of revisions per item above which should call for an optimization. */
|
||||||
const DefaultItemRevisionsThreshold = 20
|
const DefaultItemRevisionsThreshold = 20
|
||||||
@@ -25,7 +30,7 @@ const LargeEntryDeltaThreshold = 25
|
|||||||
* 2. Remote server history. Entries are automatically added by the server and must be
|
* 2. Remote server history. Entries are automatically added by the server and must be
|
||||||
* retrieved per item via an API call.
|
* retrieved per item via an API call.
|
||||||
*/
|
*/
|
||||||
export class SNHistoryManager extends AbstractService {
|
export class SNHistoryManager extends AbstractService implements HistoryServiceInterface {
|
||||||
private removeChangeObserver: () => void
|
private removeChangeObserver: () => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -343,13 +343,13 @@ export class ItemManager
|
|||||||
/**
|
/**
|
||||||
* Returns all items that an item directly references
|
* Returns all items that an item directly references
|
||||||
*/
|
*/
|
||||||
public referencesForItem(
|
public referencesForItem<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface>(
|
||||||
itemToLookupUuidFor: Models.DecryptedItemInterface,
|
itemToLookupUuidFor: Models.DecryptedItemInterface,
|
||||||
contentType?: ContentType,
|
contentType?: ContentType,
|
||||||
): Models.DecryptedItemInterface[] {
|
): I[] {
|
||||||
const item = this.findSureItem(itemToLookupUuidFor.uuid)
|
const item = this.findSureItem<I>(itemToLookupUuidFor.uuid)
|
||||||
const uuids = item.references.map((ref) => ref.uuid)
|
const uuids = item.references.map((ref) => ref.uuid)
|
||||||
let references = this.findItems(uuids)
|
let references = this.findItems<I>(uuids)
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
references = references.filter((ref) => {
|
references = references.filter((ref) => {
|
||||||
return ref?.content_type === contentType
|
return ref?.content_type === contentType
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ export class SNMigrationService extends AbstractService {
|
|||||||
await this.markMigrationsAsDone()
|
await this.markMigrationsAsDone()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await this.services.deviceInterface.setRawStorageValue(
|
await this.markMigrationsAsDone()
|
||||||
namespacedKey(this.services.identifier, RawStorageKey.SnjsVersion),
|
|
||||||
SnjsVersion,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export class SNSessionManager
|
|||||||
private httpService: HttpServiceInterface,
|
private httpService: HttpServiceInterface,
|
||||||
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||||
|
private workspaceIdentifier: string,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -130,6 +131,14 @@ export class SNSessionManager
|
|||||||
super.deinit()
|
super.deinit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getWorkspaceDisplayIdentifier(): string {
|
||||||
|
if (this.user) {
|
||||||
|
return this.user.email
|
||||||
|
} else {
|
||||||
|
return this.workspaceIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setUser(user?: User) {
|
private setUser(user?: User) {
|
||||||
this.user = user
|
this.user = user
|
||||||
this.apiService.setUser(user)
|
this.apiService.setUser(user)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ chai.use(chaiAsPromised)
|
|||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('migrations', () => {
|
describe('migrations', () => {
|
||||||
const allMigrations = ['2.0.15', '2.7.0', '2.20.0', '2.36.0', '2.42.0']
|
const allMigrations = ['2.0.15', '2.7.0', '2.20.0', '2.36.0', '2.42.0', '2.167.6']
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/snjs",
|
"name": "@standardnotes/snjs",
|
||||||
"version": "2.167.5",
|
"version": "2.167.6",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0 <17.0.0"
|
"node": ">=16.0.0 <17.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import {
|
|||||||
WebApplicationInterface,
|
WebApplicationInterface,
|
||||||
MobileDeviceInterface,
|
MobileDeviceInterface,
|
||||||
MobileUnlockTiming,
|
MobileUnlockTiming,
|
||||||
InternalEventBus,
|
|
||||||
DecryptedItem,
|
DecryptedItem,
|
||||||
EditorIdentifier,
|
EditorIdentifier,
|
||||||
FeatureIdentifier,
|
FeatureIdentifier,
|
||||||
Environment,
|
Environment,
|
||||||
ApplicationOptionsDefaults,
|
ApplicationOptionsDefaults,
|
||||||
|
BackupServiceInterface,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { makeObservable, observable } from 'mobx'
|
import { makeObservable, observable } from 'mobx'
|
||||||
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||||
@@ -93,27 +93,26 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
})
|
})
|
||||||
|
|
||||||
deviceInterface.setApplication(this)
|
deviceInterface.setApplication(this)
|
||||||
const internalEventBus = new InternalEventBus()
|
|
||||||
|
|
||||||
this.itemControllerGroup = new ItemGroupController(this)
|
this.itemControllerGroup = new ItemGroupController(this)
|
||||||
this.routeService = new RouteService(this, internalEventBus)
|
this.routeService = new RouteService(this, this.internalEventBus)
|
||||||
|
|
||||||
this.webServices = {} as WebServices
|
this.webServices = {} as WebServices
|
||||||
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
|
this.webServices.keyboardService = new KeyboardService(platform, this.environment)
|
||||||
this.webServices.archiveService = new ArchiveManager(this)
|
this.webServices.archiveService = new ArchiveManager(this)
|
||||||
this.webServices.themeService = new ThemeManager(this, internalEventBus)
|
this.webServices.themeService = new ThemeManager(this, this.internalEventBus)
|
||||||
this.webServices.autolockService = this.isNativeMobileWeb()
|
this.webServices.autolockService = this.isNativeMobileWeb()
|
||||||
? undefined
|
? undefined
|
||||||
: new AutolockService(this, internalEventBus)
|
: new AutolockService(this, this.internalEventBus)
|
||||||
this.webServices.desktopService = isDesktopDevice(deviceInterface)
|
this.webServices.desktopService = isDesktopDevice(deviceInterface)
|
||||||
? new DesktopManager(this, deviceInterface)
|
? new DesktopManager(this, deviceInterface, this.fileBackups as BackupServiceInterface)
|
||||||
: undefined
|
: undefined
|
||||||
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
|
this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface)
|
||||||
this.webServices.changelogService = new ChangelogService(this.environment, this.storage)
|
this.webServices.changelogService = new ChangelogService(this.environment, this.storage)
|
||||||
this.webServices.momentsService = new MomentsService(
|
this.webServices.momentsService = new MomentsService(
|
||||||
this,
|
this,
|
||||||
this.webServices.viewControllerManager.filesController,
|
this.webServices.viewControllerManager.filesController,
|
||||||
internalEventBus,
|
this.internalEventBus,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.isNativeMobileWeb()) {
|
if (this.isNativeMobileWeb()) {
|
||||||
@@ -181,6 +180,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
for (const observer of this.webEventObservers) {
|
for (const observer of this.webEventObservers) {
|
||||||
observer(event, data)
|
observer(event, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.internalEventBus.publish({ type: event, payload: data })
|
||||||
}
|
}
|
||||||
|
|
||||||
publishPanelDidResizeEvent(name: string, width: number, collapsed: boolean) {
|
publishPanelDidResizeEvent(name: string, width: number, collapsed: boolean) {
|
||||||
@@ -268,16 +269,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
return this.protocolUpgradeAvailable()
|
return this.protocolUpgradeAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadBackup(): void | Promise<void> {
|
performDesktopTextBackup(): void | Promise<void> {
|
||||||
if (isDesktopDevice(this.deviceInterface)) {
|
return this.getDesktopService()?.saveDesktopBackup()
|
||||||
return this.deviceInterface.downloadBackup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async signOutAndDeleteLocalBackups(): Promise<void> {
|
|
||||||
isDesktopDevice(this.deviceInterface) && (await this.deviceInterface.deleteLocalBackups())
|
|
||||||
|
|
||||||
return this.user.signOut()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isGlobalSpellcheckEnabled(): boolean {
|
isGlobalSpellcheckEnabled(): boolean {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
DesktopDeviceInterface,
|
DesktopDeviceInterface,
|
||||||
WebApplicationInterface,
|
WebApplicationInterface,
|
||||||
WebAppEvent,
|
WebAppEvent,
|
||||||
|
BackupServiceInterface,
|
||||||
|
DesktopWatchedDirectoriesChanges,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
|
|
||||||
export class DesktopManager
|
export class DesktopManager
|
||||||
@@ -27,10 +29,34 @@ export class DesktopManager
|
|||||||
dataLoaded = false
|
dataLoaded = false
|
||||||
lastSearchedText?: string
|
lastSearchedText?: string
|
||||||
|
|
||||||
constructor(application: WebApplicationInterface, private device: DesktopDeviceInterface) {
|
private textBackupsInterval: ReturnType<typeof setInterval> | undefined
|
||||||
|
private needsInitialTextBackup = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
application: WebApplicationInterface,
|
||||||
|
private device: DesktopDeviceInterface,
|
||||||
|
private backups: BackupServiceInterface,
|
||||||
|
) {
|
||||||
super(application, new InternalEventBus())
|
super(application, new InternalEventBus())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise<void> {
|
||||||
|
void this.backups.importWatchedDirectoryChanges(changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
beginTextBackupsTimer() {
|
||||||
|
if (this.textBackupsInterval) {
|
||||||
|
clearInterval(this.textBackupsInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.needsInitialTextBackup = true
|
||||||
|
|
||||||
|
const hoursInterval = 12
|
||||||
|
const seconds = hoursInterval * 60 * 60
|
||||||
|
const milliseconds = seconds * 1000
|
||||||
|
this.textBackupsInterval = setInterval(this.saveDesktopBackup, milliseconds)
|
||||||
|
}
|
||||||
|
|
||||||
get webApplication() {
|
get webApplication() {
|
||||||
return this.application as WebApplicationInterface
|
return this.application as WebApplicationInterface
|
||||||
}
|
}
|
||||||
@@ -44,14 +70,35 @@ export class DesktopManager
|
|||||||
super.onAppEvent(eventName).catch(console.error)
|
super.onAppEvent(eventName).catch(console.error)
|
||||||
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
if (eventName === ApplicationEvent.LocalDataLoaded) {
|
||||||
this.dataLoaded = true
|
this.dataLoaded = true
|
||||||
this.device.onInitialDataLoad()
|
if (this.backups.isTextBackupsEnabled()) {
|
||||||
|
this.beginTextBackupsTimer()
|
||||||
|
}
|
||||||
} else if (eventName === ApplicationEvent.MajorDataChange) {
|
} else if (eventName === ApplicationEvent.MajorDataChange) {
|
||||||
this.device.onMajorDataChange()
|
void this.saveDesktopBackup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveBackup() {
|
async saveDesktopBackup() {
|
||||||
this.device.onMajorDataChange()
|
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
|
||||||
|
|
||||||
|
const data = await this.getBackupFile()
|
||||||
|
if (data) {
|
||||||
|
await this.webApplication.fileBackups?.saveTextBackupData(data)
|
||||||
|
this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBackupFile(): Promise<string | undefined> {
|
||||||
|
const encrypted = this.application.hasProtectionSources()
|
||||||
|
const data = encrypted
|
||||||
|
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||||
|
: await this.application.createDecryptedBackupFile()
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return JSON.stringify(data, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtServerHost(): string {
|
getExtServerHost(): string {
|
||||||
@@ -111,6 +158,11 @@ export class DesktopManager
|
|||||||
|
|
||||||
windowLostFocus(): void {
|
windowLostFocus(): void {
|
||||||
this.webApplication.notifyWebEvent(WebAppEvent.WindowDidBlur)
|
this.webApplication.notifyWebEvent(WebAppEvent.WindowDidBlur)
|
||||||
|
|
||||||
|
if (this.needsInitialTextBackup) {
|
||||||
|
this.needsInitialTextBackup = false
|
||||||
|
void this.saveDesktopBackup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onComponentInstallationComplete(componentData: DecryptedTransferPayload<ComponentContent>) {
|
async onComponentInstallationComplete(componentData: DecryptedTransferPayload<ComponentContent>) {
|
||||||
@@ -136,25 +188,4 @@ export class DesktopManager
|
|||||||
observer.callback(updatedComponent as SNComponent)
|
observer.callback(updatedComponent as SNComponent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestBackupFile(): Promise<string | undefined> {
|
|
||||||
const encrypted = this.application.hasProtectionSources()
|
|
||||||
const data = encrypted
|
|
||||||
? await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
|
||||||
: await this.application.createDecryptedBackupFile()
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return JSON.stringify(data, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
didBeginBackup() {
|
|
||||||
this.webApplication.notifyWebEvent(WebAppEvent.BeganBackupDownload)
|
|
||||||
}
|
|
||||||
|
|
||||||
didFinishBackup(success: boolean) {
|
|
||||||
this.webApplication.notifyWebEvent(WebAppEvent.EndedBackupDownload, { success })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,7 @@ export {
|
|||||||
FileBackupReadToken,
|
FileBackupReadToken,
|
||||||
FileBackupReadChunkResponse,
|
FileBackupReadChunkResponse,
|
||||||
FileDownloadProgress,
|
FileDownloadProgress,
|
||||||
|
PlaintextBackupsMapping,
|
||||||
|
DesktopWatchedDirectoriesChanges,
|
||||||
|
DesktopWatchedDirectoriesChange,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||||
import { STRING_SIGN_OUT_CONFIRMATION } from '@/Constants/Strings'
|
import { STRING_SIGN_OUT_CONFIRMATION } from '@/Constants/Strings'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||||
@@ -8,6 +8,7 @@ import { isDesktopApplication } from '@/Utils'
|
|||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import AlertDialog from '../AlertDialog/AlertDialog'
|
import AlertDialog from '../AlertDialog/AlertDialog'
|
||||||
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -16,30 +17,24 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewControllerManager, applicationGroup }) => {
|
const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewControllerManager, applicationGroup }) => {
|
||||||
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false)
|
const hasAnyBackupsEnabled =
|
||||||
|
application.fileBackups?.isFilesBackupsEnabled() ||
|
||||||
|
application.fileBackups?.isPlaintextBackupsEnabled() ||
|
||||||
|
application.fileBackups?.isTextBackupsEnabled()
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null)
|
const cancelRef = useRef<HTMLButtonElement>(null)
|
||||||
const closeDialog = useCallback(() => {
|
const closeDialog = useCallback(() => {
|
||||||
viewControllerManager.accountMenuController.setSigningOut(false)
|
viewControllerManager.accountMenuController.setSigningOut(false)
|
||||||
}, [viewControllerManager.accountMenuController])
|
}, [viewControllerManager.accountMenuController])
|
||||||
|
|
||||||
const [localBackupsCount, setLocalBackupsCount] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
application.desktopDevice?.localBackupsCount().then(setLocalBackupsCount).catch(console.error)
|
|
||||||
}, [viewControllerManager.accountMenuController.signingOut, application.desktopDevice])
|
|
||||||
|
|
||||||
const workspaces = applicationGroup.getDescriptors()
|
const workspaces = applicationGroup.getDescriptors()
|
||||||
const showWorkspaceWarning = workspaces.length > 1 && isDesktopApplication()
|
const showWorkspaceWarning = workspaces.length > 1 && isDesktopApplication()
|
||||||
|
|
||||||
const confirm = useCallback(() => {
|
const confirm = useCallback(() => {
|
||||||
if (deleteLocalBackups) {
|
application.user.signOut().catch(console.error)
|
||||||
application.signOutAndDeleteLocalBackups().catch(console.error)
|
|
||||||
} else {
|
|
||||||
application.user.signOut().catch(console.error)
|
|
||||||
}
|
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}, [application, closeDialog, deleteLocalBackups])
|
}, [application, closeDialog])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog closeDialog={closeDialog}>
|
<AlertDialog closeDialog={closeDialog}>
|
||||||
@@ -66,31 +61,26 @@ const ConfirmSignoutModal: FunctionComponent<Props> = ({ application, viewContro
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{localBackupsCount > 0 && (
|
{hasAnyBackupsEnabled && (
|
||||||
<div className="flex">
|
<>
|
||||||
<div className="sk-panel-row"></div>
|
<HorizontalSeparator classes="my-2" />
|
||||||
<label className="flex items-center">
|
<div className="flex">
|
||||||
<input
|
<div className="sk-panel-row"></div>
|
||||||
type="checkbox"
|
<div>
|
||||||
checked={deleteLocalBackups}
|
<p className="text-base text-foreground lg:text-sm">
|
||||||
onChange={(event) => {
|
Local backups are enabled for this workspace. Review your backup files manually to decide what to keep.
|
||||||
setDeleteLocalBackups((event.target as HTMLInputElement).checked)
|
</p>
|
||||||
}}
|
<button
|
||||||
/>
|
className="sk-a mt-2 cursor-pointer rounded p-0 capitalize lg:text-sm"
|
||||||
<span className="ml-2">
|
onClick={() => {
|
||||||
Delete {localBackupsCount} local backup file
|
void application.fileBackups?.openAllDirectoriesContainingBackupFiles()
|
||||||
{localBackupsCount > 1 ? 's' : ''}
|
}}
|
||||||
</span>
|
>
|
||||||
</label>
|
View backup files
|
||||||
<button
|
</button>
|
||||||
className="sk-a ml-1.5 cursor-pointer rounded p-0 capitalize"
|
</div>
|
||||||
onClick={() => {
|
</div>
|
||||||
application.desktopDevice?.viewlocalBackups()
|
</>
|
||||||
}}
|
|
||||||
>
|
|
||||||
View backup files
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex justify-end gap-2">
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }>
|
|||||||
>
|
>
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
|
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
|
||||||
<div className="text-xs text-neutral">{backupInfo.absolutePath}</div>
|
<div className="text-xs text-neutral">{application.fileBackups?.getFileBackupAbsolutePath(backupInfo)}</div>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processPasswordChange() {
|
async processPasswordChange() {
|
||||||
await this.application.downloadBackup()
|
await this.application.performDesktopTextBackup()
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
lockContinue: true,
|
lockContinue: true,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const processEmailChange = useCallback(async () => {
|
const processEmailChange = useCallback(async () => {
|
||||||
await application.downloadBackup()
|
await application.performDesktopTextBackup()
|
||||||
|
|
||||||
setLockContinue(true)
|
setLockContinue(true)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import DataBackups from './DataBackups'
|
|||||||
import EmailBackups from './EmailBackups'
|
import EmailBackups from './EmailBackups'
|
||||||
import FileBackupsCrossPlatform from './Files/FileBackupsCrossPlatform'
|
import FileBackupsCrossPlatform from './Files/FileBackupsCrossPlatform'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import TextBackupsCrossPlatform from './TextBackups/TextBackupsCrossPlatform'
|
||||||
|
import PlaintextBackupsCrossPlatform from './PlaintextBackups/PlaintextBackupsCrossPlatform'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
viewControllerManager: ViewControllerManager
|
viewControllerManager: ViewControllerManager
|
||||||
@@ -16,6 +18,8 @@ const Backups: FunctionComponent<Props> = ({ application, viewControllerManager
|
|||||||
return (
|
return (
|
||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
<DataBackups application={application} viewControllerManager={viewControllerManager} />
|
<DataBackups application={application} viewControllerManager={viewControllerManager} />
|
||||||
|
<TextBackupsCrossPlatform application={application} />
|
||||||
|
<PlaintextBackupsCrossPlatform />
|
||||||
<FileBackupsCrossPlatform application={application} />
|
<FileBackupsCrossPlatform application={application} />
|
||||||
<EmailBackups application={application} />
|
<EmailBackups application={application} />
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isDesktopApplication } from '@/Utils'
|
|
||||||
import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services'
|
import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services'
|
||||||
import {
|
import {
|
||||||
STRING_IMPORT_SUCCESS,
|
STRING_IMPORT_SUCCESS,
|
||||||
@@ -15,7 +14,7 @@ import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef,
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Title, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||||
@@ -177,14 +176,7 @@ const DataBackups = ({ application, viewControllerManager }: Props) => {
|
|||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>Data Backups</Title>
|
<Title>Data Backups</Title>
|
||||||
|
<Subtitle>Download a backup of all your text-based data</Subtitle>
|
||||||
{isDesktopApplication() && (
|
|
||||||
<Text className="mb-3">
|
|
||||||
Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Subtitle>Download a backup of all your data</Subtitle>
|
|
||||||
|
|
||||||
{isEncryptionEnabled && (
|
{isEncryptionEnabled && (
|
||||||
<form className="sk-panel-form sk-panel-row">
|
<form className="sk-panel-form sk-panel-row">
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const EmailBackups = ({ application }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`${!hasAccount ? 'pointer-events-none cursor-default opacity-50' : ''}`}>
|
<div className={`${!hasAccount ? 'pointer-events-none cursor-default opacity-50' : ''}`}>
|
||||||
<Subtitle>Email frequency</Subtitle>
|
<Subtitle>Frequency</Subtitle>
|
||||||
<Text>How often to receive backups.</Text>
|
<Text>How often to receive backups.</Text>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ const FileBackupsCrossPlatform = ({ application }: Props) => {
|
|||||||
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
||||||
|
|
||||||
return fileBackupsService ? (
|
return fileBackupsService ? (
|
||||||
<FileBackupsDesktop application={application} backupsService={fileBackupsService} />
|
<FileBackupsDesktop backupsService={fileBackupsService} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>File Backups</Title>
|
<Title>Automatic File Backups</Title>
|
||||||
<Subtitle>Automatically save encrypted backups of files uploaded on any device to this computer.</Subtitle>
|
<Subtitle>Automatically save encrypted backups of your files.</Subtitle>
|
||||||
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
|
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
<HorizontalSeparator classes="my-4" />
|
<HorizontalSeparator classes="my-4" />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import Switch from '@/Components/Switch/Switch'
|
import Switch from '@/Components/Switch/Switch'
|
||||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||||
@@ -10,30 +9,21 @@ import BackupsDropZone from './BackupsDropZone'
|
|||||||
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
||||||
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||||
|
import { BackupServiceInterface } from '@standardnotes/snjs'
|
||||||
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
backupsService: BackupServiceInterface
|
||||||
backupsService: NonNullable<WebApplication['fileBackups']>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileBackupsDesktop = ({ application, backupsService }: Props) => {
|
const FileBackupsDesktop = ({ backupsService }: Props) => {
|
||||||
const [backupsEnabled, setBackupsEnabled] = useState(false)
|
const application = useApplication()
|
||||||
const [backupsLocation, setBackupsLocation] = useState('')
|
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isFilesBackupsEnabled())
|
||||||
|
const [backupsLocation, setBackupsLocation] = useState(backupsService.getFilesBackupsLocation())
|
||||||
useEffect(() => {
|
|
||||||
void backupsService.isFilesBackupsEnabled().then(setBackupsEnabled)
|
|
||||||
}, [backupsService])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (backupsEnabled) {
|
|
||||||
void backupsService.getFilesBackupsLocation().then(setBackupsLocation)
|
|
||||||
}
|
|
||||||
}, [backupsService, backupsEnabled])
|
|
||||||
|
|
||||||
const changeBackupsLocation = useCallback(async () => {
|
const changeBackupsLocation = useCallback(async () => {
|
||||||
await backupsService.changeFilesBackupsLocation()
|
const newLocation = await backupsService.changeFilesBackupsLocation()
|
||||||
|
setBackupsLocation(newLocation)
|
||||||
setBackupsLocation(await backupsService.getFilesBackupsLocation())
|
|
||||||
}, [backupsService])
|
}, [backupsService])
|
||||||
|
|
||||||
const openBackupsLocation = useCallback(async () => {
|
const openBackupsLocation = useCallback(async () => {
|
||||||
@@ -42,25 +32,24 @@ const FileBackupsDesktop = ({ application, backupsService }: Props) => {
|
|||||||
|
|
||||||
const toggleBackups = useCallback(async () => {
|
const toggleBackups = useCallback(async () => {
|
||||||
if (backupsEnabled) {
|
if (backupsEnabled) {
|
||||||
await backupsService.disableFilesBackups()
|
backupsService.disableFilesBackups()
|
||||||
} else {
|
} else {
|
||||||
await backupsService.enableFilesBackups()
|
await backupsService.enableFilesBackups()
|
||||||
}
|
}
|
||||||
|
|
||||||
setBackupsEnabled(await backupsService.isFilesBackupsEnabled())
|
setBackupsEnabled(backupsService.isFilesBackupsEnabled())
|
||||||
|
setBackupsLocation(backupsService.getFilesBackupsLocation())
|
||||||
}, [backupsService, backupsEnabled])
|
}, [backupsService, backupsEnabled])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>File Backups</Title>
|
<Title>Automatic File Backups</Title>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="mr-10 flex flex-col">
|
<div className="mr-10 flex flex-col">
|
||||||
<Subtitle>
|
<Subtitle>Automatically save encrypted backups of your uploaded files to this computer.</Subtitle>
|
||||||
Automatically save encrypted backups of files uploaded on any device to this computer.
|
|
||||||
</Subtitle>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
||||||
</div>
|
</div>
|
||||||
@@ -85,14 +74,14 @@ const FileBackupsDesktop = ({ application, backupsService }: Props) => {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<EncryptionStatusItem
|
<EncryptionStatusItem
|
||||||
status={backupsLocation}
|
status={backupsLocation || 'Not Set'}
|
||||||
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
||||||
checkmark={false}
|
checkmark={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-2.5 flex flex-row">
|
<div className="mt-2.5 flex flex-row">
|
||||||
<Button label="Open Backups Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||||
<Button label="Change Backups Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
|
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||||
|
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||||
|
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import PlaintextBackupsDesktop from './PlaintextBackupsDesktop'
|
||||||
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
|
||||||
|
const PlaintextBackupsCrossPlatform = () => {
|
||||||
|
const application = useApplication()
|
||||||
|
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
||||||
|
|
||||||
|
return fileBackupsService ? (
|
||||||
|
<PlaintextBackupsDesktop backupsService={fileBackupsService} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Automatic Plaintext Backups</Title>
|
||||||
|
<Subtitle>Automatically save backups of all your notes into plaintext, non-encrypted folders.</Subtitle>
|
||||||
|
<Text className="mt-3">To enable plaintext backups, use the Standard Notes desktop application.</Text>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlaintextBackupsCrossPlatform
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import Button from '@/Components/Button/Button'
|
||||||
|
import Switch from '@/Components/Switch/Switch'
|
||||||
|
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
||||||
|
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||||
|
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||||
|
import { BackupServiceInterface } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
backupsService: BackupServiceInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlaintextBackupsDesktop = ({ backupsService }: Props) => {
|
||||||
|
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isPlaintextBackupsEnabled())
|
||||||
|
const [backupsLocation, setBackupsLocation] = useState(backupsService.getPlaintextBackupsLocation())
|
||||||
|
|
||||||
|
const changeBackupsLocation = useCallback(async () => {
|
||||||
|
const newLocation = await backupsService.changePlaintextBackupsLocation()
|
||||||
|
setBackupsLocation(newLocation)
|
||||||
|
}, [backupsService])
|
||||||
|
|
||||||
|
const openBackupsLocation = useCallback(async () => {
|
||||||
|
await backupsService.openPlaintextBackupsLocation()
|
||||||
|
}, [backupsService])
|
||||||
|
|
||||||
|
const toggleBackups = useCallback(async () => {
|
||||||
|
if (backupsEnabled) {
|
||||||
|
backupsService.disablePlaintextBackups()
|
||||||
|
} else {
|
||||||
|
await backupsService.enablePlaintextBackups()
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackupsEnabled(backupsService.isPlaintextBackupsEnabled())
|
||||||
|
setBackupsLocation(backupsService.getPlaintextBackupsLocation())
|
||||||
|
}, [backupsEnabled, backupsService])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Automatic Plaintext Backups</Title>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="mr-10 flex flex-col">
|
||||||
|
<Subtitle>
|
||||||
|
Automatically save backups of all your notes to this computer into plaintext, non-encrypted folders.
|
||||||
|
</Subtitle>
|
||||||
|
</div>
|
||||||
|
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!backupsEnabled && (
|
||||||
|
<>
|
||||||
|
<HorizontalSeparator classes="mt-2.5 mb-4" />
|
||||||
|
<Text>Plaintext backups are not enabled. Enable to choose where your data is backed up.</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PreferencesSegment>
|
||||||
|
|
||||||
|
{backupsEnabled && (
|
||||||
|
<>
|
||||||
|
<HorizontalSeparator classes="my-4" />
|
||||||
|
<PreferencesSegment>
|
||||||
|
<>
|
||||||
|
<Text className="mb-3">Plaintext backups are enabled and saved to:</Text>
|
||||||
|
<EncryptionStatusItem
|
||||||
|
status={backupsLocation || 'Not Set'}
|
||||||
|
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
||||||
|
checkmark={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2.5 flex flex-row">
|
||||||
|
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||||
|
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PreferencesGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(PlaintextBackupsDesktop)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
|
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||||
|
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import TextBackupsDesktop from './TextBackupsDesktop'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextBackupsCrossPlatform = ({ application }: Props) => {
|
||||||
|
const fileBackupsService = useMemo(() => application.fileBackups, [application])
|
||||||
|
|
||||||
|
return fileBackupsService ? (
|
||||||
|
<TextBackupsDesktop backupsService={fileBackupsService} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Automatic Text Backups</Title>
|
||||||
|
<Subtitle>Automatically save encrypted and decrypted backups of your note and tag data.</Subtitle>
|
||||||
|
<Text className="mt-3">To enable text backups, use the Standard Notes desktop application.</Text>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextBackupsCrossPlatform
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { Title, Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import Button from '@/Components/Button/Button'
|
||||||
|
import Switch from '@/Components/Switch/Switch'
|
||||||
|
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
import EncryptionStatusItem from '../../Security/EncryptionStatusItem'
|
||||||
|
import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup'
|
||||||
|
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||||
|
import { BackupServiceInterface } from '@standardnotes/snjs'
|
||||||
|
import { useApplication } from '@/Components/ApplicationProvider'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
backupsService: BackupServiceInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextBackupsDesktop = ({ backupsService }: Props) => {
|
||||||
|
const application = useApplication()
|
||||||
|
const [backupsEnabled, setBackupsEnabled] = useState(backupsService.isTextBackupsEnabled())
|
||||||
|
const [backupsLocation, setBackupsLocation] = useState(backupsService.getTextBackupsLocation())
|
||||||
|
|
||||||
|
const changeBackupsLocation = useCallback(async () => {
|
||||||
|
const newLocation = await backupsService.changeTextBackupsLocation()
|
||||||
|
setBackupsLocation(newLocation)
|
||||||
|
}, [backupsService])
|
||||||
|
|
||||||
|
const openBackupsLocation = useCallback(async () => {
|
||||||
|
await backupsService.openTextBackupsLocation()
|
||||||
|
}, [backupsService])
|
||||||
|
|
||||||
|
const toggleBackups = useCallback(async () => {
|
||||||
|
if (backupsEnabled) {
|
||||||
|
backupsService.disableTextBackups()
|
||||||
|
} else {
|
||||||
|
await backupsService.enableTextBackups()
|
||||||
|
}
|
||||||
|
|
||||||
|
setBackupsEnabled(backupsService.isTextBackupsEnabled())
|
||||||
|
setBackupsLocation(backupsService.getTextBackupsLocation())
|
||||||
|
}, [backupsEnabled, backupsService])
|
||||||
|
|
||||||
|
const performBackup = useCallback(async () => {
|
||||||
|
void application.getDesktopService()?.saveDesktopBackup()
|
||||||
|
}, [application])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>Automatic Encrypted Text Backups</Title>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="mr-10 flex flex-col">
|
||||||
|
<Subtitle>
|
||||||
|
Automatically save encrypted text backups of all your note and tag data to this computer.
|
||||||
|
</Subtitle>
|
||||||
|
</div>
|
||||||
|
<Switch onChange={toggleBackups} checked={backupsEnabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!backupsEnabled && (
|
||||||
|
<>
|
||||||
|
<HorizontalSeparator classes="mt-2.5 mb-4" />
|
||||||
|
<Text>Text backups are not enabled. Enable to choose where your data is backed up.</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PreferencesSegment>
|
||||||
|
|
||||||
|
{backupsEnabled && (
|
||||||
|
<>
|
||||||
|
<HorizontalSeparator classes="my-4" />
|
||||||
|
|
||||||
|
<PreferencesSegment>
|
||||||
|
<>
|
||||||
|
<Text className="mb-3">Text backups are enabled and saved to:</Text>
|
||||||
|
|
||||||
|
<EncryptionStatusItem
|
||||||
|
status={backupsLocation || 'Not Set'}
|
||||||
|
icon={<Icon type="attachment-file" className="min-h-5 min-w-5" />}
|
||||||
|
checkmark={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-2.5 flex flex-row">
|
||||||
|
<Button label="Open Location" className={'mr-3 text-xs'} onClick={openBackupsLocation} />
|
||||||
|
<Button label="Change Location" className={'mr-3 text-xs'} onClick={changeBackupsLocation} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<HorizontalSeparator classes="my-4" />
|
||||||
|
|
||||||
|
<Text className="mb-3">
|
||||||
|
Backups are saved automatically throughout the day. You can perform a one-time backup now below.
|
||||||
|
</Text>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Button label="Perform Backup" className={'mr-3 text-xs'} onClick={performBackup} />
|
||||||
|
</div>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PreferencesGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(TextBackupsDesktop)
|
||||||
@@ -12,7 +12,7 @@ const TwoFactorTitle: FunctionComponent<Props> = ({ auth }) => {
|
|||||||
return <Title>Two-factor authentication not available</Title>
|
return <Title>Two-factor authentication not available</Title>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Title>Two-factor authentication</Title>
|
return <Title>Two-Factor Authentication</Title>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(TwoFactorTitle)
|
export default observer(TwoFactorTitle)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
import { InternalEventBus, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
|
import { InternalEventBusInterface, InternalEventPublishStrategy, removeFromArray } from '@standardnotes/snjs'
|
||||||
import { WebApplication } from '../../Application/Application'
|
import { WebApplication } from '../../Application/Application'
|
||||||
import { Disposer } from '@/Types/Disposer'
|
import { Disposer } from '@/Types/Disposer'
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export abstract class AbstractViewController<Event = void, EventData = void> {
|
|||||||
protected disposers: Disposer[] = []
|
protected disposers: Disposer[] = []
|
||||||
private eventObservers: ControllerEventObserver<Event, EventData>[] = []
|
private eventObservers: ControllerEventObserver<Event, EventData>[] = []
|
||||||
|
|
||||||
constructor(public application: WebApplication, protected eventBus: InternalEventBus) {}
|
constructor(public application: WebApplication, protected eventBus: InternalEventBusInterface) {}
|
||||||
|
|
||||||
protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise<void> {
|
protected async publishCrossControllerEventSync(name: CrossControllerEvent, data?: unknown): Promise<void> {
|
||||||
await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE)
|
await this.eventBus.publishSync({ type: name, payload: data }, InternalEventPublishStrategy.SEQUENCE)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||||
import { ApplicationEvent, InternalEventBus, StorageKey } from '@standardnotes/services'
|
import { ApplicationEvent, InternalEventBusInterface, StorageKey } from '@standardnotes/services'
|
||||||
import { isDev } from '@/Utils'
|
import { isDev } from '@/Utils'
|
||||||
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
||||||
import { FilesController } from '../FilesController'
|
import { FilesController } from '../FilesController'
|
||||||
@@ -19,7 +19,11 @@ export class MomentsService extends AbstractViewController {
|
|||||||
isEnabled = false
|
isEnabled = false
|
||||||
private intervalReference: ReturnType<typeof setInterval> | undefined
|
private intervalReference: ReturnType<typeof setInterval> | undefined
|
||||||
|
|
||||||
constructor(application: WebApplication, private filesController: FilesController, eventBus: InternalEventBus) {
|
constructor(
|
||||||
|
application: WebApplication,
|
||||||
|
private filesController: FilesController,
|
||||||
|
eventBus: InternalEventBusInterface,
|
||||||
|
) {
|
||||||
super(application, eventBus)
|
super(application, eventBus)
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { PreferenceId, RootQueryParam } from '@standardnotes/ui-services'
|
|||||||
import { AbstractViewController } from './Abstract/AbstractViewController'
|
import { AbstractViewController } from './Abstract/AbstractViewController'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
|
||||||
const DEFAULT_PANE: PreferenceId = 'account'
|
const DEFAULT_PANE: PreferenceId = 'backups'
|
||||||
|
|
||||||
export class PreferencesController extends AbstractViewController {
|
export class PreferencesController extends AbstractViewController {
|
||||||
private _open = false
|
private _open = true
|
||||||
currentPane: PreferenceId = DEFAULT_PANE
|
currentPane: PreferenceId = DEFAULT_PANE
|
||||||
|
|
||||||
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||||
|
|||||||
Reference in New Issue
Block a user