Files
standardnotes-app-web/packages/desktop/app/javascripts/Main/Store.ts
2022-06-07 11:52:15 -05:00

186 lines
5.8 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import { MessageType } from '../../../test/TestIpcMessage'
import { Language } from './SpellcheckerManager'
import { ensureIsBoolean, isTesting, isDev, isBoolean } from './Utils/Utils'
import { FileDoesNotExist } from './Utils/FileUtils'
import { BackupsDirectoryName } from './Backups/BackupsManager'
import { handleTestMessage } from './Utils/Testing'
const app = process.type === 'browser' ? require('electron').app : require('@electron/remote').app
function logError(...message: any) {
console.error('store:', ...message)
}
export enum StoreKeys {
ExtServerHost = 'extServerHost',
UseSystemMenuBar = 'useSystemMenuBar',
MenuBarVisible = 'isMenuBarVisible',
BackupsLocation = 'backupsLocation',
BackupsDisabled = 'backupsDisabled',
MinimizeToTray = 'minimizeToTray',
EnableAutoUpdate = 'enableAutoUpdates',
ZoomFactor = 'zoomFactor',
SelectedSpellCheckerLanguageCodes = 'selectedSpellCheckerLanguageCodes',
UseNativeKeychain = 'useNativeKeychain',
FileBackupsEnabled = 'fileBackupsEnabled',
FileBackupsLocation = 'fileBackupsLocation',
}
interface StoreData {
[StoreKeys.ExtServerHost]: string
[StoreKeys.UseSystemMenuBar]: boolean
[StoreKeys.MenuBarVisible]: boolean
[StoreKeys.BackupsLocation]: string
[StoreKeys.BackupsDisabled]: boolean
[StoreKeys.MinimizeToTray]: boolean
[StoreKeys.EnableAutoUpdate]: boolean
[StoreKeys.UseNativeKeychain]: boolean | null
[StoreKeys.ZoomFactor]: number
[StoreKeys.SelectedSpellCheckerLanguageCodes]: Set<Language> | null
[StoreKeys.FileBackupsEnabled]: boolean
[StoreKeys.FileBackupsLocation]: string
}
function createSanitizedStoreData(data: any = {}): StoreData {
return {
[StoreKeys.MenuBarVisible]: ensureIsBoolean(data[StoreKeys.MenuBarVisible], true),
[StoreKeys.UseSystemMenuBar]: ensureIsBoolean(data[StoreKeys.UseSystemMenuBar], false),
[StoreKeys.BackupsDisabled]: ensureIsBoolean(data[StoreKeys.BackupsDisabled], false),
[StoreKeys.MinimizeToTray]: ensureIsBoolean(data[StoreKeys.MinimizeToTray], false),
[StoreKeys.EnableAutoUpdate]: ensureIsBoolean(data[StoreKeys.EnableAutoUpdate], true),
[StoreKeys.UseNativeKeychain]: isBoolean(data[StoreKeys.UseNativeKeychain])
? data[StoreKeys.UseNativeKeychain]
: null,
[StoreKeys.ExtServerHost]: data[StoreKeys.ExtServerHost],
[StoreKeys.BackupsLocation]: sanitizeBackupsLocation(data[StoreKeys.BackupsLocation]),
[StoreKeys.ZoomFactor]: sanitizeZoomFactor(data[StoreKeys.ZoomFactor]),
[StoreKeys.SelectedSpellCheckerLanguageCodes]: sanitizeSpellCheckerLanguageCodes(
data[StoreKeys.SelectedSpellCheckerLanguageCodes],
),
[StoreKeys.FileBackupsEnabled]: ensureIsBoolean(data[StoreKeys.FileBackupsEnabled], false),
[StoreKeys.FileBackupsLocation]: data[StoreKeys.FileBackupsLocation],
}
}
function sanitizeZoomFactor(factor?: any): number {
if (typeof factor === 'number' && factor > 0) {
return factor
} else {
return 1
}
}
function sanitizeBackupsLocation(location?: unknown): string {
const defaultPath = path.join(
isTesting() ? app.getPath('userData') : isDev() ? app.getPath('documents') : app.getPath('home'),
BackupsDirectoryName,
)
if (typeof location !== 'string') {
return defaultPath
}
try {
const stat = fs.lstatSync(location)
if (stat.isDirectory()) {
return location
}
/** Path points to something other than a directory */
return defaultPath
} catch (e) {
/** Path does not point to a valid directory */
logError(e)
return defaultPath
}
}
function sanitizeSpellCheckerLanguageCodes(languages?: unknown): Set<Language> | null {
if (!languages) {
return null
}
if (!Array.isArray(languages)) {
return null
}
const set = new Set<Language>()
const validLanguages = Object.values(Language)
for (const language of languages) {
if (validLanguages.includes(language)) {
set.add(language)
}
}
return set
}
export function serializeStoreData(data: StoreData): string {
return JSON.stringify(data, (_key, value) => {
if (value instanceof Set) {
return Array.from(value)
}
return value
})
}
function parseDataFile(filePath: string) {
try {
const fileData = fs.readFileSync(filePath)
const userData = JSON.parse(fileData.toString())
return createSanitizedStoreData(userData)
} catch (error: any) {
console.log('Error reading store file', error)
if (error.code !== FileDoesNotExist) {
logError(error)
}
return createSanitizedStoreData({})
}
}
export class Store {
static instance: Store
readonly path: string
readonly data: StoreData
static getInstance(): Store {
if (!this.instance) {
/**
* Renderer process has to get `app` module via `remote`, whereas the main process
* can get it directly app.getPath('userData') will return a string of the user's
* app data directory path.
* TODO(baptiste): stop using Store in the renderer process.
*/
const userDataPath = app.getPath('userData')
this.instance = new Store(userDataPath)
}
return this.instance
}
static get<T extends keyof StoreData>(key: T): StoreData[T] {
return this.getInstance().get(key)
}
constructor(userDataPath: string) {
this.path = path.join(userDataPath, 'user-preferences.json')
this.data = parseDataFile(this.path)
if (isTesting()) {
handleTestMessage(MessageType.StoreSettingsLocation, () => this.path)
handleTestMessage(MessageType.StoreSet, (key, value) => {
this.set(key, value)
})
}
}
get<T extends keyof StoreData>(key: T): StoreData[T] {
return this.data[key]
}
set<T extends keyof StoreData>(key: T, val: StoreData[T]): void {
this.data[key] = val
fs.writeFileSync(this.path, serializeStoreData(this.data))
}
}