From 85c0ec0d134d2d3eaf19dbfbe1133d0a15075d1c Mon Sep 17 00:00:00 2001 From: John Knox Date: Mon, 7 Oct 2019 08:49:05 -0700 Subject: [PATCH] Persist settings in ${XDG_CONFIG_HOME}/flipper/settings Summary: Moves the settings state from electron local storage into a json file in the users configured config location. Unless modified by the user, this will usually be `~/.config/flipper/settings.json` Settings will now persist across re-installs, and can now be easily inspected and backed up. Reviewed By: passy Differential Revision: D17712687 fbshipit-source-id: 1e778063e41d0a1a86145817b9797bf0458121da --- src/chrome/SettingsSheet.tsx | 2 +- src/reducers/index.tsx | 17 +++++++++++++--- src/reducers/settings.tsx | 19 +++++------------- src/utils/jsonFileReduxPersistStorage.tsx | 24 +++++++++++++++-------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/chrome/SettingsSheet.tsx b/src/chrome/SettingsSheet.tsx index 2f3402ad8..23119c140 100644 --- a/src/chrome/SettingsSheet.tsx +++ b/src/chrome/SettingsSheet.tsx @@ -163,6 +163,6 @@ class SignInSheet extends Component { } export default connect( - ({settingsState}) => ({settings: settingsState.settings}), + ({settingsState}) => ({settings: settingsState}), {updateSettings}, )(SignInSheet); diff --git a/src/reducers/index.tsx b/src/reducers/index.tsx index fac725e99..fc7a5cf6b 100644 --- a/src/reducers/index.tsx +++ b/src/reducers/index.tsx @@ -27,11 +27,14 @@ import plugins, { Action as PluginsAction, } from './plugins'; import settings, { - State as SettingsState, + Settings as SettingsState, Action as SettingsAction, } from './settings'; import user, {State as UserState, Action as UserAction} from './user'; - +import JsonFileStorage from '../utils/jsonFileReduxPersistStorage'; +import os from 'os'; +import {resolve} from 'path'; +import xdg from 'xdg-basedir'; import {persistReducer, PersistPartial} from 'redux-persist'; import {Store as ReduxStore, MiddlewareAPI as ReduxMiddlewareAPI} from 'redux'; @@ -61,6 +64,14 @@ export type State = { export type Store = ReduxStore; export type MiddlewareAPI = ReduxMiddlewareAPI, State>; +const settingsStorage = new JsonFileStorage( + resolve( + ...(xdg.config ? [xdg.config] : [os.homedir(), '.config']), + 'flipper', + 'settings.json', + ), +); + export default combineReducers({ application: persistReducer( { @@ -101,7 +112,7 @@ export default combineReducers({ user, ), settingsState: persistReducer( - {key: 'settings', storage, whitelist: ['settings']}, + {key: 'settings', storage: settingsStorage}, settings, ), }); diff --git a/src/reducers/settings.tsx b/src/reducers/settings.tsx index b36cccb7b..466d00dda 100644 --- a/src/reducers/settings.tsx +++ b/src/reducers/settings.tsx @@ -11,10 +11,6 @@ export type Settings = { androidHome: string; }; -export type State = { - settings: Settings; -}; - export type Action = | {type: 'INIT'} | { @@ -22,21 +18,16 @@ export type Action = payload: Settings; }; -const initialState: State = { - settings: { - androidHome: '/opt/android_sdk', - }, +const initialState: Settings = { + androidHome: '/opt/android_sdk', }; export default function reducer( - state: State = initialState, + state: Settings = initialState, action: Actions, -): State { +): Settings { if (action.type === 'UPDATE_SETTINGS') { - return { - ...state, - settings: action.payload, - }; + return action.payload; } return state; } diff --git a/src/utils/jsonFileReduxPersistStorage.tsx b/src/utils/jsonFileReduxPersistStorage.tsx index 8459f0f7d..bfbe31775 100644 --- a/src/utils/jsonFileReduxPersistStorage.tsx +++ b/src/utils/jsonFileReduxPersistStorage.tsx @@ -5,7 +5,9 @@ * @format */ -import {promises} from 'fs'; +import {promises, exists} from 'fs'; +import path from 'path'; +import {promisify} from 'util'; /** * Redux-persist storage engine for storing state in a human readable JSON file. @@ -28,14 +30,12 @@ export default class JsonFileStorage { .then(buffer => buffer.toString()) .then(this.deserializeValue) .catch(e => { - console.error( + console.warn( `Failed to read settings file: "${ this.filepath }". ${e}. Replacing file with default settings.`, ); - return promises - .writeFile(this.filepath, JSON.stringify({})) - .then(() => ({})); + return this.writeContents(JSON.stringify({})).then(() => ({})); }); } @@ -51,7 +51,7 @@ export default class JsonFileStorage { setItem(_key: string, value: any, callback?: (_: any) => any): Promise { const originalValue = this.parseFile(); const writePromise = originalValue.then(_ => - promises.writeFile(this.filepath, this.serializeValue(value)), + this.writeContents(this.serializeValue(value)), ); return Promise.all([originalValue, writePromise]).then(([o, _]) => { @@ -61,8 +61,7 @@ export default class JsonFileStorage { } removeItem(_key: string, callback?: () => any): Promise { - return promises - .writeFile(this.filepath, JSON.stringify({})) + return this.writeContents(JSON.stringify({})) .then(_ => callback && callback()) .then(() => {}); } @@ -86,4 +85,13 @@ export default class JsonFileStorage { }, {}); return JSON.stringify(reconstructedObject); } + + writeContents(content: string): Promise { + const dir = path.dirname(this.filepath); + return promisify(exists)(dir) + .then(dirExists => + dirExists ? Promise.resolve() : promises.mkdir(dir, {recursive: true}), + ) + .then(() => promises.writeFile(this.filepath, content)); + } }