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
This commit is contained in:
John Knox
2019-10-07 08:49:05 -07:00
committed by Facebook Github Bot
parent 7775e82851
commit 85c0ec0d13
4 changed files with 36 additions and 26 deletions

View File

@@ -163,6 +163,6 @@ class SignInSheet extends Component<Props, State> {
} }
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>( export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({settingsState}) => ({settings: settingsState.settings}), ({settingsState}) => ({settings: settingsState}),
{updateSettings}, {updateSettings},
)(SignInSheet); )(SignInSheet);

View File

@@ -27,11 +27,14 @@ import plugins, {
Action as PluginsAction, Action as PluginsAction,
} from './plugins'; } from './plugins';
import settings, { import settings, {
State as SettingsState, Settings as SettingsState,
Action as SettingsAction, Action as SettingsAction,
} from './settings'; } from './settings';
import user, {State as UserState, Action as UserAction} from './user'; 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 {persistReducer, PersistPartial} from 'redux-persist';
import {Store as ReduxStore, MiddlewareAPI as ReduxMiddlewareAPI} from 'redux'; import {Store as ReduxStore, MiddlewareAPI as ReduxMiddlewareAPI} from 'redux';
@@ -61,6 +64,14 @@ export type State = {
export type Store = ReduxStore<State, Actions>; export type Store = ReduxStore<State, Actions>;
export type MiddlewareAPI = ReduxMiddlewareAPI<Dispatch<Actions>, State>; export type MiddlewareAPI = ReduxMiddlewareAPI<Dispatch<Actions>, State>;
const settingsStorage = new JsonFileStorage(
resolve(
...(xdg.config ? [xdg.config] : [os.homedir(), '.config']),
'flipper',
'settings.json',
),
);
export default combineReducers<State, Actions>({ export default combineReducers<State, Actions>({
application: persistReducer<ApplicationState, Actions>( application: persistReducer<ApplicationState, Actions>(
{ {
@@ -101,7 +112,7 @@ export default combineReducers<State, Actions>({
user, user,
), ),
settingsState: persistReducer( settingsState: persistReducer(
{key: 'settings', storage, whitelist: ['settings']}, {key: 'settings', storage: settingsStorage},
settings, settings,
), ),
}); });

View File

@@ -11,10 +11,6 @@ export type Settings = {
androidHome: string; androidHome: string;
}; };
export type State = {
settings: Settings;
};
export type Action = export type Action =
| {type: 'INIT'} | {type: 'INIT'}
| { | {
@@ -22,21 +18,16 @@ export type Action =
payload: Settings; payload: Settings;
}; };
const initialState: State = { const initialState: Settings = {
settings: { androidHome: '/opt/android_sdk',
androidHome: '/opt/android_sdk',
},
}; };
export default function reducer( export default function reducer(
state: State = initialState, state: Settings = initialState,
action: Actions, action: Actions,
): State { ): Settings {
if (action.type === 'UPDATE_SETTINGS') { if (action.type === 'UPDATE_SETTINGS') {
return { return action.payload;
...state,
settings: action.payload,
};
} }
return state; return state;
} }

View File

@@ -5,7 +5,9 @@
* @format * @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. * 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(buffer => buffer.toString())
.then(this.deserializeValue) .then(this.deserializeValue)
.catch(e => { .catch(e => {
console.error( console.warn(
`Failed to read settings file: "${ `Failed to read settings file: "${
this.filepath this.filepath
}". ${e}. Replacing file with default settings.`, }". ${e}. Replacing file with default settings.`,
); );
return promises return this.writeContents(JSON.stringify({})).then(() => ({}));
.writeFile(this.filepath, JSON.stringify({}))
.then(() => ({}));
}); });
} }
@@ -51,7 +51,7 @@ export default class JsonFileStorage {
setItem(_key: string, value: any, callback?: (_: any) => any): Promise<any> { setItem(_key: string, value: any, callback?: (_: any) => any): Promise<any> {
const originalValue = this.parseFile(); const originalValue = this.parseFile();
const writePromise = originalValue.then(_ => const writePromise = originalValue.then(_ =>
promises.writeFile(this.filepath, this.serializeValue(value)), this.writeContents(this.serializeValue(value)),
); );
return Promise.all([originalValue, writePromise]).then(([o, _]) => { return Promise.all([originalValue, writePromise]).then(([o, _]) => {
@@ -61,8 +61,7 @@ export default class JsonFileStorage {
} }
removeItem(_key: string, callback?: () => any): Promise<void> { removeItem(_key: string, callback?: () => any): Promise<void> {
return promises return this.writeContents(JSON.stringify({}))
.writeFile(this.filepath, JSON.stringify({}))
.then(_ => callback && callback()) .then(_ => callback && callback())
.then(() => {}); .then(() => {});
} }
@@ -86,4 +85,13 @@ export default class JsonFileStorage {
}, {}); }, {});
return JSON.stringify(reconstructedObject); return JSON.stringify(reconstructedObject);
} }
writeContents(content: string): Promise<void> {
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));
}
} }