From 30a24485097898a91c8d61309cc3b37f0a48b46c Mon Sep 17 00:00:00 2001 From: John Furrow Date: Wed, 7 Jun 2017 21:52:54 -0700 Subject: [PATCH] Ensure that only files belonging to torrent are deleted --- .../constants/clientRequestServiceEvents.js | 1 + server/constants/fileListPropMap.js | 53 ++++++++ server/models/ClientRequest.js | 8 -- server/models/client.js | 38 ------ server/routes/client.js | 12 +- server/services/clientRequestService.js | 119 +++++++++++++++--- server/services/historyService.js | 23 +--- server/services/torrentService.js | 33 +++-- server/util/methodCallUtil.js | 30 +++++ 9 files changed, 214 insertions(+), 103 deletions(-) create mode 100644 server/constants/fileListPropMap.js create mode 100644 server/util/methodCallUtil.js diff --git a/server/constants/clientRequestServiceEvents.js b/server/constants/clientRequestServiceEvents.js index b2006f2a..cf105a72 100644 --- a/server/constants/clientRequestServiceEvents.js +++ b/server/constants/clientRequestServiceEvents.js @@ -7,6 +7,7 @@ const clientRequestServiceEvents = [ 'PROCESS_TORRENT_LIST_END', 'PROCESS_TORRENT_LIST_START', 'PROCESS_TRANSFER_RATE_START', + 'TORRENTS_REMOVED' ]; module.exports = objectUtil.createSymbolMapFromArray( diff --git a/server/constants/fileListPropMap.js b/server/constants/fileListPropMap.js new file mode 100644 index 00000000..b93ee659 --- /dev/null +++ b/server/constants/fileListPropMap.js @@ -0,0 +1,53 @@ +'use strict'; +const fileListPropMap = new Map(); +const defaultTransformer = value => value; + +fileListPropMap.set( + 'path', + { + methodCall: 'f.path=', + transformValue: defaultTransformer + } +); + +fileListPropMap.set( + 'pathComponents', + { + methodCall: 'f.path_components=', + transformValue: defaultTransformer + } +); + +fileListPropMap.set( + 'priority', + { + methodCall: 'f.priority=', + transformValue: defaultTransformer + } +); + +fileListPropMap.set( + 'sizeBytes', + { + methodCall: 'f.size_bytes=', + transformValue: Number + } +); + +fileListPropMap.set( + 'sizeChunks', + { + methodCall: 'f.size_chunks=', + transformValue: Number + } +); + +fileListPropMap.set( + 'completedChunks', + { + methodCall: 'f.completed_chunks=', + transformValue: Number + } +); + +module.exports = fileListPropMap; diff --git a/server/models/ClientRequest.js b/server/models/ClientRequest.js index c58418c0..db597614 100644 --- a/server/models/ClientRequest.js +++ b/server/models/ClientRequest.js @@ -260,14 +260,6 @@ class ClientRequest { }); } - removeTorrents(options) { - let hashes = this.getEnsuredArray(options.hashes); - - hashes.forEach((hash) => { - this.requests.push(this.getMethodCall('d.erase', [hash])); - }); - } - setDownloadPath(options) { let hashes = this.getEnsuredArray(options.hashes); diff --git a/server/models/client.js b/server/models/client.js index 0a554678..681ee9bd 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -10,7 +10,6 @@ const clientResponseUtil = require('../util/clientResponseUtil'); const clientSettingsMap = require('../../shared/constants/clientSettingsMap'); const ClientRequest = require('./ClientRequest'); const formatUtil = require('../../shared/util/formatUtil'); -const scgi = require('../util/scgi'); const TemporaryStorage = require('./TemporaryStorage'); const torrentFilePropsMap = require('../../shared/constants/torrentFilePropsMap'); const torrentPeerPropsMap = require('../../shared/constants/torrentPeerPropsMap'); @@ -78,43 +77,6 @@ var client = { request.send(); }, - deleteTorrents: (options, callback) => { - let filesToDelete = null; - let eraseTorrentsRequest = new ClientRequest(); - - eraseTorrentsRequest.removeTorrents({hashes: options.hashes}); - eraseTorrentsRequest.onComplete((response, error) => { - if (options.deleteData) { - const torrents = torrentCollection.torrents; - - options.hashes.forEach(hash => { - let fileToDelete = null; - const torrent = torrents[hash]; - - if (torrent.isMultiFile && torrent.directory != null) { - fileToDelete = torrent.directory; - } else if (torrent.directory != null && torrent.name != null) { - fileToDelete = path.join(torrent.directory, torrent.name); - } - - if (fileToDelete != null) { - rimraf(fileToDelete, {disableGlob: true}, error => { - if (error) { - console.error(`Error deleting file: ${fileToDelete}\n${error}`); - } - }); - } - }); - } - - torrentService.fetchTorrentList(); - - callback(response, error); - }); - - eraseTorrentsRequest.send(); - }, - downloadFiles(hash, files, res) { try { let selectedTorrent = null; diff --git a/server/routes/client.js b/server/routes/client.js index 81b37a07..a238b749 100644 --- a/server/routes/client.js +++ b/server/routes/client.js @@ -4,6 +4,7 @@ const multer = require('multer'); const ajaxUtil = require('../util/ajaxUtil'); const client = require('../models/client'); +const clientRequestService = require('../services/clientRequestService'); const router = express.Router(); const upload = multer({ @@ -65,10 +66,15 @@ router.post('/torrents/move', function(req, res, next) { }); router.post('/torrents/delete', function(req, res, next) { - let deleteData = req.body.deleteData; - let hashes = req.body.hash; + const {deleteData, hash: hashes} = req.body; + const callback = ajaxUtil.getResponseFn(res); - client.deleteTorrents({hashes, deleteData}, ajaxUtil.getResponseFn(res)); + clientRequestService + .removeTorrents({hashes, deleteData}) + .then(callback) + .catch((err) => { + callback(null, response); + }); }); router.get('/torrents/taxonomy', function(req, res, next) { diff --git a/server/services/clientRequestService.js b/server/services/clientRequestService.js index 665dc155..31e3dd0a 100644 --- a/server/services/clientRequestService.js +++ b/server/services/clientRequestService.js @@ -1,8 +1,18 @@ 'use strict'; const EventEmitter = require('events'); +const path = require('path'); +const rimraf = require('rimraf'); const clientRequestServiceEvents = require('../constants/clientRequestServiceEvents'); +const fileListPropMap = require('../constants/fileListPropMap'); +const methodCallUtil = require('../util/methodCallUtil'); const scgi = require('../util/scgi'); +const torrentListPropMap = require('../constants/torrentListPropMap'); + +const fileListMethodCallConfig = methodCallUtil.getMethodCallConfigFromPropMap( + fileListPropMap, + ['pathComponents'] +); class ClientRequestService extends EventEmitter { constructor() { @@ -33,6 +43,89 @@ class ClientRequestService extends EventEmitter { this.torrentListReducers.push(reducer); } + removeTorrents(options = {hashes: [], deleteData: false}) { + const methodCalls = options.hashes.reduce( + (accumulator, hash, index) => { + let eraseFileMethodCallIndex = index; + + // If we're deleting files, we grab each torrents' file list before we + // remove them. + if (options.deleteData) { + // We offset the indices of these method calls so that we know exactly + // where to retrieve them in the future. + const directoryBaseMethodCallIndex = index + options.hashes.length; + eraseFileMethodCallIndex = index + options.hashes.length * 2; + + accumulator[index] = { + methodName: 'f.multicall', + params: [hash, ''].concat(fileListMethodCallConfig.methodCalls) + }; + + accumulator[directoryBaseMethodCallIndex] = { + methodName: 'd.directory_base', + params: [hash] + }; + } + + accumulator[eraseFileMethodCallIndex] = { + methodName: 'd.erase', + params: [hash] + }; + + return accumulator; + }, + [] + ); + + return scgi + .methodCall('system.multicall', [methodCalls]) + .then((response) => { + if (options.deleteData) { + const torrentCount = options.hashes.length; + const filesToDelete = options.hashes.reduce( + (accumulator, hash, hashIndex) => { + const fileList = response[hashIndex][0]; + const directoryBase = response[hashIndex + torrentCount][0]; + + const filesToDelete = fileList.reduce( + (fileListAccumulator, file) => { + // We only look at the first path component returned because + // if it's a directory within the torrent, then we'll remove + // the entire directory. + const filePath = path.join(directoryBase, file[0][0]); + + // filePath might be a directory, so it may have already been + // added. If not, we add it. + if (!fileListAccumulator.includes(filePath)) { + fileListAccumulator.push(filePath); + } + + return fileListAccumulator; + }, + [] + ); + + return accumulator.concat(filesToDelete); + }, + [] + ); + + filesToDelete.forEach(file => { + rimraf(file, {disableGlob: true}, error => { + if (error) { + console.error(`Error deleting file: ${file}\n${error}`); + } + }); + }); + } + + this.emit(clientRequestServiceEvents.TORRENTS_REMOVED, options); + + return response; + }) + .catch(clientError => this.processClientError(clientError)); + } + /** * Sends a multicall request to rTorrent with the requested method calls. * @@ -48,15 +141,10 @@ class ClientRequestService extends EventEmitter { * with the processed client error. */ fetchTorrentList(options) { - return new Promise((resolve, reject) => { - scgi.methodCall('d.multicall2', ['', 'main'].concat(options.methodCalls)) - .then((torrentList) => { - resolve(this.processTorrentListResponse(torrentList, options)); - }) - .catch((clientError) => { - reject(this.processClientError(clientError)); - }); - }); + return scgi + .methodCall('d.multicall2', ['', 'main'].concat(options.methodCalls)) + .then(torrents => this.processTorrentListResponse(torrents, options)) + .catch(clientError => this.processClientError(clientError)); } fetchTransferSummary(options) { @@ -64,15 +152,14 @@ class ClientRequestService extends EventEmitter { return {methodName, params: []}; }); - return new Promise((resolve, reject) => { - scgi.methodCall('system.multicall', [methodCalls]) - .then((transferRate) => { - resolve(this.processTransferRateResponse(transferRate, options)); + return scgi + .methodCall('system.multicall', [methodCalls]) + .then(transferRate => { + return this.processTransferRateResponse(transferRate, options); }) - .catch((clientError) => { - reject(this.processClientError(clientError)); + .catch(clientError => { + return this.processClientError(clientError); }); - }); } processClientError(error) { diff --git a/server/services/historyService.js b/server/services/historyService.js index d82155d9..fc7def2e 100644 --- a/server/services/historyService.js +++ b/server/services/historyService.js @@ -6,27 +6,12 @@ const config = require('../../config'); const HistoryEra = require('../models/HistoryEra'); const historyServiceEvents = require('../constants/historyServiceEvents'); const historySnapshotTypes = require('../../shared/constants/historySnapshotTypes'); +const methodCallUtil = require('../util/methodCallUtil'); const objectUtil = require('../../shared/util/objectUtil'); const transferSummaryPropMap = require('../constants/transferSummaryPropMap'); -const transferSummaryFetchOptions = Array - .from(transferSummaryPropMap.keys()) - .reduce( - (accumulator, key) => { - const {methodCall, transformValue} = transferSummaryPropMap.get(key); - - accumulator.methodCalls.push(methodCall); - accumulator.propLabels.push(key); - accumulator.valueTransformations.push(transformValue); - - return accumulator; - }, - { - methodCalls: [], - propLabels: [], - valueTransformations: [] - } - ); +const transferSummaryMethodCallConfig = methodCallUtil + .getMethodCallConfigFromPropMap(transferSummaryPropMap); const processData = (opts, callback, data, error) => { if (error) { @@ -159,7 +144,7 @@ class HistoryService extends EventEmitter { } clientRequestService - .fetchTransferSummary(transferSummaryFetchOptions) + .fetchTransferSummary(transferSummaryMethodCallConfig) .then(this.handleFetchTransferSummarySuccess.bind(this)) .catch(this.handleFetchTransferSummaryError.bind(this)); } diff --git a/server/services/torrentService.js b/server/services/torrentService.js index c97e4358..011a598f 100644 --- a/server/services/torrentService.js +++ b/server/services/torrentService.js @@ -5,30 +5,15 @@ const config = require('../../config.js'); const clientRequestService = require('./clientRequestService.js'); const clientRequestServiceEvents = require('../constants/clientRequestServiceEvents'); const formatUtil = require('../../shared/util/formatUtil'); +const methodCallUtil = require('../util/methodCallUtil'); const notificationService = require('./notificationService.js'); const serverEventTypes = require('../../shared/constants/serverEventTypes'); const torrentListPropMap = require('../constants/torrentListPropMap'); const torrentServiceEvents = require('../constants/torrentServiceEvents.js'); const torrentStatusMap = require('../../shared/constants/torrentStatusMap'); -const torrentListFetchOptions = Array - .from(torrentListPropMap.keys()) - .reduce( - (accumulator, key) => { - const {methodCall, transformValue} = torrentListPropMap.get(key); - - accumulator.methodCalls.push(methodCall); - accumulator.propLabels.push(key); - accumulator.valueTransformations.push(transformValue); - - return accumulator; - }, - { - methodCalls: [], - propLabels: [], - valueTransformations: [] - } - ); +const torrentListMethodCallConfig = methodCallUtil + .getMethodCallConfigFromPropMap(torrentListPropMap); class TorrentService extends EventEmitter { constructor() { @@ -40,6 +25,7 @@ class TorrentService extends EventEmitter { this.fetchTorrentList = this.fetchTorrentList.bind(this); this.handleTorrentProcessed = this.handleTorrentProcessed.bind(this); + this.handleTorrentsRemoved = this.handleTorrentsRemoved.bind(this); clientRequestService.addTorrentListReducer({ key: 'status', @@ -61,6 +47,11 @@ class TorrentService extends EventEmitter { this.handleTorrentProcessed ); + clientRequestService.on( + clientRequestServiceEvents.TORRENTS_REMOVED, + this.handleTorrentsRemoved + ); + this.fetchTorrentList(); } @@ -117,7 +108,7 @@ class TorrentService extends EventEmitter { } clientRequestService - .fetchTorrentList(torrentListFetchOptions) + .fetchTorrentList(torrentListMethodCallConfig) .then(this.handleFetchTorrentListSuccess.bind(this)) .catch(this.handleFetchTorrentListError.bind(this)); } @@ -321,6 +312,10 @@ class TorrentService extends EventEmitter { && nextData.percentComplete === 100 ); } + + handleTorrentsRemoved() { + this.fetchTorrentList(); + } } module.exports = new TorrentService(); diff --git a/server/util/methodCallUtil.js b/server/util/methodCallUtil.js new file mode 100644 index 00000000..a26d69fd --- /dev/null +++ b/server/util/methodCallUtil.js @@ -0,0 +1,30 @@ +'use strict'; + +const methodCallUtil = { + getMethodCallConfigFromPropMap(map = new Map(), requestedKeys) { + let desiredKeys = Array.from(map.keys()); + + if (requestedKeys != null) { + desiredKeys = desiredKeys.filter(key => requestedKeys.includes(key)); + } + + return desiredKeys.reduce( + (accumulator, key) => { + const {methodCall, transformValue} = map.get(key); + + accumulator.methodCalls.push(methodCall); + accumulator.propLabels.push(key); + accumulator.valueTransformations.push(transformValue); + + return accumulator; + }, + { + methodCalls: [], + propLabels: [], + valueTransformations: [] + } + ); + } +}; + +module.exports = methodCallUtil;