From 98666ea51eb573a687ac0df5fa8df5a48e8eeef9 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Mon, 12 Oct 2020 21:23:52 +0800 Subject: [PATCH] server: migrate Flood settings functions to per-user service --- .../src/javascript/actions/TorrentActions.ts | 3 +- .../src/javascript/constants/ServerActions.ts | 2 +- client/src/javascript/stores/TorrentStore.ts | 19 +++- server/models/settings.ts | 95 ---------------- server/routes/api/index.ts | 80 +++++++++---- server/routes/api/torrents.ts | 7 -- server/services/index.ts | 13 +++ server/services/settingService.ts | 107 ++++++++++++++++++ 8 files changed, 199 insertions(+), 127 deletions(-) delete mode 100644 server/models/settings.ts create mode 100644 server/services/settingService.ts diff --git a/client/src/javascript/actions/TorrentActions.ts b/client/src/javascript/actions/TorrentActions.ts index bed20880..b50da17f 100644 --- a/client/src/javascript/actions/TorrentActions.ts +++ b/client/src/javascript/actions/TorrentActions.ts @@ -32,6 +32,7 @@ const TorrentActions = { type: 'CLIENT_ADD_TORRENT_SUCCESS', data: { count: options.urls.length, + start: options.start, destination: options.destination, response, }, @@ -57,6 +58,7 @@ const TorrentActions = { type: 'CLIENT_ADD_TORRENT_SUCCESS', data: { count: options.files.length, + start: options.start, destination: options.destination, data, }, @@ -79,7 +81,6 @@ const TorrentActions = { type: 'CLIENT_ADD_TORRENT_SUCCESS', data: { count: 1, - destination: '', }, }); download(response.data, (options.name || `${Date.now()}`).concat('.torrent')); diff --git a/client/src/javascript/constants/ServerActions.ts b/client/src/javascript/constants/ServerActions.ts index d82eab61..aef57bcb 100644 --- a/client/src/javascript/constants/ServerActions.ts +++ b/client/src/javascript/constants/ServerActions.ts @@ -233,7 +233,7 @@ interface ClientFetchTorrentDetailsSuccessAction { interface ClientAddTorrentSuccessAction { type: 'CLIENT_ADD_TORRENT_SUCCESS'; - data: {count: number; destination: string}; + data: {count: number; start?: boolean; destination?: string}; } interface ClientMoveTorrentsSuccessAction { diff --git a/client/src/javascript/stores/TorrentStore.ts b/client/src/javascript/stores/TorrentStore.ts index acbf4882..98e164db 100644 --- a/client/src/javascript/stores/TorrentStore.ts +++ b/client/src/javascript/stores/TorrentStore.ts @@ -99,15 +99,28 @@ class TorrentStoreClass extends BaseStore { this.emit('CLIENT_ADD_TORRENT_ERROR'); } - handleAddTorrentSuccess(response: {count: number; destination: string}) { + handleAddTorrentSuccess({count, start, destination}: {count: number; start?: boolean; destination?: string}) { this.emit('CLIENT_ADD_TORRENT_SUCCESS'); - SettingsStore.setFloodSetting('torrentDestination', response.destination); + // Remembers user's selections + const changedSettings: Partial = { + ...(start != null + ? { + startTorrentsOnLoad: start, + } + : {}), + ...(destination != null + ? { + torrentDestination: destination, + } + : {}), + }; + SettingsStore.saveFloodSettings(changedSettings); AlertStore.add({ accumulation: { id: 'alert.torrent.add', - value: response.count || 1, + value: count || 1, }, id: 'alert.torrent.add', }); diff --git a/server/models/settings.ts b/server/models/settings.ts deleted file mode 100644 index 1ff149c0..00000000 --- a/server/models/settings.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Datastore from 'nedb'; -import noop from 'lodash/noop'; -import path from 'path'; - -import type {UserInDatabase} from '@shared/schema/Auth'; - -import config from '../../config'; - -interface SettingsRecord { - id: string; - data: unknown; -} - -const databases = new Map(); - -function getDb(user: UserInDatabase): Datastore { - const userId = user._id; - - if (databases.has(userId)) { - return databases.get(userId); - } - - const database = new Datastore({ - autoload: true, - filename: path.join(config.dbPath, userId, 'settings', 'settings.db'), - }); - - databases.set(userId, database); - - return database; -} - -const settings = { - get: ( - user: UserInDatabase, - opts: {property?: string}, - callback: (data: Record | null, error?: Error) => void, - ) => { - const query: {id?: string} = {}; - const settingsToReturn: Record = {}; - - if (opts.property) { - query.id = opts.property; - } - - getDb(user) - .find(query) - .exec((err, docs) => { - if (err) { - callback(null, err); - return; - } - - docs.forEach((doc) => { - settingsToReturn[doc.id] = doc.data; - }); - - callback(settingsToReturn); - }); - }, - - set: ( - user: UserInDatabase, - payloads: Array, - callback: (data?: Array> | null, error?: Error) => void = noop, - ) => { - const docsResponse: Array> = []; - - if (payloads && payloads.length) { - let error; - payloads.forEach((payload) => { - getDb(user).update( - {id: payload.id}, - {$set: {data: payload.data}}, - {upsert: true}, - (err: Error | null, _numberOfUpdated: number, docs: Array, _upsert: boolean) => { - docsResponse.push(docs); - if (err) { - error = err; - } - }, - ); - }); - if (error) { - callback(null, error); - } else { - callback(docsResponse); - } - } else { - callback(); - } - }, -}; - -export default settings; diff --git a/server/routes/api/index.ts b/server/routes/api/index.ts index 73b2069b..80ff40d3 100644 --- a/server/routes/api/index.ts +++ b/server/routes/api/index.ts @@ -1,6 +1,7 @@ import express from 'express'; import passport from 'passport'; +import type {FloodSettings} from '@shared/types/FloodSettings'; import type {HistorySnapshot} from '@shared/constants/historySnapshotTypes'; import type {NotificationFetchOptions} from '@shared/types/Notification'; import type {SetFloodSettingsOptions} from '@shared/types/api/index'; @@ -13,7 +14,6 @@ import clientActivityStream from '../../middleware/clientActivityStream'; import eventStream from '../../middleware/eventStream'; import feedMonitorRoutes from './feed-monitor'; import {getDirectoryList} from '../../util/fileUtil'; -import settings from '../../models/settings'; import torrentsRoutes from './torrents'; const router = express.Router(); @@ -54,29 +54,69 @@ router.delete('/notifications', (req, res) => { req.services?.notificationService.clearNotifications(ajaxUtil.getResponseFn(res)); }); +/** + * GET /api/settings + * @summary Gets all Flood's settings + * @tags Flood + * @security User + * @return {FloodSettings} 200 - success response - application/json + * @return {Error} 500 - failure response - application/json + */ router.get('/settings', (req, res) => { - if (req.user == null) { - res.status(401).json(Error('Unauthorized')); - return; - } - settings.get(req.user, req.query, ajaxUtil.getResponseFn(res)); + const callback = ajaxUtil.getResponseFn(res); + + req.services?.settingService + .get(null) + .then((settings) => { + callback(settings as FloodSettings); + }) + .catch((err) => { + callback(null, err); + }); }); +/** + * GET /api/settings/{property} + * @summary Gets Flood's settings + * @tags Flood + * @security User + * @param property.path + * @return {Partial} 200 - success response - application/json + * @return {Error} 500 - failure response - application/json + */ +router.get('/settings/:property', (req, res) => { + const callback = ajaxUtil.getResponseFn(res); + + req.services?.settingService + .get(req.params.property) + .then((settings) => { + callback(settings); + }) + .catch((err) => { + callback(null, err); + }); +}); + +/** + * PATCH /api/settings + * @summary Sets Flood's settings + * @tags Flood + * @security User + * @param {Partial} request.body.required - options - application/json + * @return {Partial} 200 - success response - application/json + * @return {Error} 500 - failure response - application/json + */ router.patch('/settings', (req, res) => { - Promise.all( - Object.keys(req.body).map(async (property) => { - return { - id: property, - data: req.body[property as keyof typeof req.body], - }; - }), - ).then((settingRecords) => { - if (req.user == null) { - res.status(401).json(Error('Unauthorized')); - return; - } - settings.set(req.user, settingRecords, ajaxUtil.getResponseFn(res)); - }); + const callback = ajaxUtil.getResponseFn(res); + + req.services?.settingService + .set(req.body) + .then((savedSettings) => { + callback(savedSettings); + }) + .catch((err) => { + callback(null, err); + }); }); export default router; diff --git a/server/routes/api/torrents.ts b/server/routes/api/torrents.ts index b7c60c2f..f7f1fc9a 100644 --- a/server/routes/api/torrents.ts +++ b/server/routes/api/torrents.ts @@ -23,7 +23,6 @@ import ajaxUtil from '../../util/ajaxUtil'; import client from '../../models/client'; import {getTempPath} from '../../models/TemporaryStorage'; import mediainfo from '../../util/mediainfo'; -import settings from '../../models/settings'; const router = express.Router(); @@ -42,9 +41,6 @@ router.post('/add-urls', (req, res) => req.services?.clientGatewayService .addTorrentsByURL(req.body) .then((response) => { - if (req.user != null) { - settings.set(req.user, [{id: 'startTorrentsOnLoad', data: req.body.start === true}]); - } req.services?.torrentService.fetchTorrentList(); return response; }) @@ -69,9 +65,6 @@ router.post('/add-files', (req, res) req.services?.clientGatewayService .addTorrentsByFile(req.body) .then((response) => { - if (req.user != null) { - settings.set(req.user, [{id: 'startTorrentsOnLoad', data: req.body.start === true}]); - } req.services?.torrentService.fetchTorrentList(); return response; }) diff --git a/server/services/index.ts b/server/services/index.ts index c6cedf3a..2096e7d8 100644 --- a/server/services/index.ts +++ b/server/services/index.ts @@ -5,6 +5,7 @@ import ClientRequestManager from './rTorrent/clientRequestManager'; import FeedService from './feedService'; import HistoryService from './historyService'; import NotificationService from './notificationService'; +import SettingService from './settingService'; import TaxonomyService from './taxonomyService'; import TorrentService from './torrentService'; @@ -14,6 +15,7 @@ type Service = | typeof FeedService | typeof HistoryService | typeof NotificationService + | typeof SettingService | typeof TaxonomyService | typeof TorrentService; @@ -23,6 +25,7 @@ const serviceInstances: { feedServices: Record; historyServices: Record; notificationServices: Record; + settingServices: Record; taxonomyServices: Record; torrentServices: Record; } = { @@ -31,6 +34,7 @@ const serviceInstances: { feedServices: {}, historyServices: {}, notificationServices: {}, + settingServices: {}, taxonomyServices: {}, torrentServices: {}, }; @@ -70,6 +74,10 @@ const getNotificationService = (user: UserInDatabase): NotificationService => { return getService('notificationServices', NotificationService, user); }; +const getSettingService = (user: UserInDatabase): SettingService => { + return getService('settingServices', SettingService, user); +}; + const getTaxonomyService = (user: UserInDatabase): TaxonomyService => { return getService('taxonomyServices', TaxonomyService, user); }; @@ -100,6 +108,10 @@ const getAllServices = (user: UserInDatabase) => return getNotificationService(user); }, + get settingService() { + return getSettingService(user); + }, + get taxonomyService() { return getTaxonomyService(user); }, @@ -157,6 +169,7 @@ export default { getClientGatewayService, getHistoryService, getNotificationService, + getSettingService, getTaxonomyService, getTorrentService, }; diff --git a/server/services/settingService.ts b/server/services/settingService.ts new file mode 100644 index 00000000..0674111c --- /dev/null +++ b/server/services/settingService.ts @@ -0,0 +1,107 @@ +import Datastore from 'nedb'; +import path from 'path'; + +import type {FloodSettings} from '@shared/types/FloodSettings'; + +import config from '../../config'; +import BaseService from './BaseService'; + +interface SettingRecord { + id: string; + data: unknown; +} + +interface SettingServiceEvents { + SETTINGS_CHANGE: (changeSettings: Partial) => void; +} + +class SettingService extends BaseService { + db = this.loadDatabase(); + + loadDatabase(): Datastore { + const userId = this.user._id; + + const database = new Datastore({ + autoload: true, + filename: path.join(config.dbPath, userId, 'settings', 'settings.db'), + }); + + return database; + } + + async get(property: string | null): Promise> { + return new Promise((resolve, reject) => { + this.db + .find( + property + ? { + id: property, + } + : {}, + ) + .exec(async (err, docs) => { + if (err) { + reject(err); + } + + resolve( + Object.assign( + {}, + ...(await Promise.all( + docs.map(async (doc) => { + return { + [doc.id]: doc.data, + }; + }), + )), + ), + ); + }); + }); + } + + async set(changedSettings: Partial): Promise> { + const savedSettings: typeof changedSettings = {}; + + if (changedSettings) { + let error: Error | null = null; + + await Promise.all( + Object.keys(changedSettings).map((key) => { + return new Promise((resolve, reject) => { + const property = key as keyof FloodSettings; + return this.db.update( + {id: property}, + {$set: {data: changedSettings[property]}}, + {upsert: true}, + (err: Error | null) => { + if (err) { + reject(err); + return; + } + resolve(property); + }, + ); + }) + .then((property) => { + Object.assign(savedSettings, { + [property]: changedSettings[property], + }); + }) + .catch((e) => { + error = e; + }); + }), + ); + + if (error) { + return Promise.reject(error); + } + } + + this.emit('SETTINGS_CHANGE', savedSettings); + return savedSettings; + } +} + +export default SettingService;