Ensure that only files belonging to torrent are deleted

This commit is contained in:
John Furrow
2017-06-07 21:52:54 -07:00
parent a0e6621873
commit 30a2448509
9 changed files with 214 additions and 103 deletions

View File

@@ -7,6 +7,7 @@ const clientRequestServiceEvents = [
'PROCESS_TORRENT_LIST_END', 'PROCESS_TORRENT_LIST_END',
'PROCESS_TORRENT_LIST_START', 'PROCESS_TORRENT_LIST_START',
'PROCESS_TRANSFER_RATE_START', 'PROCESS_TRANSFER_RATE_START',
'TORRENTS_REMOVED'
]; ];
module.exports = objectUtil.createSymbolMapFromArray( module.exports = objectUtil.createSymbolMapFromArray(

View File

@@ -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;

View File

@@ -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) { setDownloadPath(options) {
let hashes = this.getEnsuredArray(options.hashes); let hashes = this.getEnsuredArray(options.hashes);

View File

@@ -10,7 +10,6 @@ const clientResponseUtil = require('../util/clientResponseUtil');
const clientSettingsMap = require('../../shared/constants/clientSettingsMap'); const clientSettingsMap = require('../../shared/constants/clientSettingsMap');
const ClientRequest = require('./ClientRequest'); const ClientRequest = require('./ClientRequest');
const formatUtil = require('../../shared/util/formatUtil'); const formatUtil = require('../../shared/util/formatUtil');
const scgi = require('../util/scgi');
const TemporaryStorage = require('./TemporaryStorage'); const TemporaryStorage = require('./TemporaryStorage');
const torrentFilePropsMap = require('../../shared/constants/torrentFilePropsMap'); const torrentFilePropsMap = require('../../shared/constants/torrentFilePropsMap');
const torrentPeerPropsMap = require('../../shared/constants/torrentPeerPropsMap'); const torrentPeerPropsMap = require('../../shared/constants/torrentPeerPropsMap');
@@ -78,43 +77,6 @@ var client = {
request.send(); 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) { downloadFiles(hash, files, res) {
try { try {
let selectedTorrent = null; let selectedTorrent = null;

View File

@@ -4,6 +4,7 @@ const multer = require('multer');
const ajaxUtil = require('../util/ajaxUtil'); const ajaxUtil = require('../util/ajaxUtil');
const client = require('../models/client'); const client = require('../models/client');
const clientRequestService = require('../services/clientRequestService');
const router = express.Router(); const router = express.Router();
const upload = multer({ const upload = multer({
@@ -65,10 +66,15 @@ router.post('/torrents/move', function(req, res, next) {
}); });
router.post('/torrents/delete', function(req, res, next) { router.post('/torrents/delete', function(req, res, next) {
let deleteData = req.body.deleteData; const {deleteData, hash: hashes} = req.body;
let hashes = req.body.hash; 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) { router.get('/torrents/taxonomy', function(req, res, next) {

View File

@@ -1,8 +1,18 @@
'use strict'; 'use strict';
const EventEmitter = require('events'); const EventEmitter = require('events');
const path = require('path');
const rimraf = require('rimraf');
const clientRequestServiceEvents = require('../constants/clientRequestServiceEvents'); const clientRequestServiceEvents = require('../constants/clientRequestServiceEvents');
const fileListPropMap = require('../constants/fileListPropMap');
const methodCallUtil = require('../util/methodCallUtil');
const scgi = require('../util/scgi'); const scgi = require('../util/scgi');
const torrentListPropMap = require('../constants/torrentListPropMap');
const fileListMethodCallConfig = methodCallUtil.getMethodCallConfigFromPropMap(
fileListPropMap,
['pathComponents']
);
class ClientRequestService extends EventEmitter { class ClientRequestService extends EventEmitter {
constructor() { constructor() {
@@ -33,6 +43,89 @@ class ClientRequestService extends EventEmitter {
this.torrentListReducers.push(reducer); 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. * Sends a multicall request to rTorrent with the requested method calls.
* *
@@ -48,15 +141,10 @@ class ClientRequestService extends EventEmitter {
* with the processed client error. * with the processed client error.
*/ */
fetchTorrentList(options) { fetchTorrentList(options) {
return new Promise((resolve, reject) => { return scgi
scgi.methodCall('d.multicall2', ['', 'main'].concat(options.methodCalls)) .methodCall('d.multicall2', ['', 'main'].concat(options.methodCalls))
.then((torrentList) => { .then(torrents => this.processTorrentListResponse(torrents, options))
resolve(this.processTorrentListResponse(torrentList, options)); .catch(clientError => this.processClientError(clientError));
})
.catch((clientError) => {
reject(this.processClientError(clientError));
});
});
} }
fetchTransferSummary(options) { fetchTransferSummary(options) {
@@ -64,14 +152,13 @@ class ClientRequestService extends EventEmitter {
return {methodName, params: []}; return {methodName, params: []};
}); });
return new Promise((resolve, reject) => { return scgi
scgi.methodCall('system.multicall', [methodCalls]) .methodCall('system.multicall', [methodCalls])
.then((transferRate) => { .then(transferRate => {
resolve(this.processTransferRateResponse(transferRate, options)); return this.processTransferRateResponse(transferRate, options);
}) })
.catch((clientError) => { .catch(clientError => {
reject(this.processClientError(clientError)); return this.processClientError(clientError);
});
}); });
} }

View File

@@ -6,27 +6,12 @@ const config = require('../../config');
const HistoryEra = require('../models/HistoryEra'); const HistoryEra = require('../models/HistoryEra');
const historyServiceEvents = require('../constants/historyServiceEvents'); const historyServiceEvents = require('../constants/historyServiceEvents');
const historySnapshotTypes = require('../../shared/constants/historySnapshotTypes'); const historySnapshotTypes = require('../../shared/constants/historySnapshotTypes');
const methodCallUtil = require('../util/methodCallUtil');
const objectUtil = require('../../shared/util/objectUtil'); const objectUtil = require('../../shared/util/objectUtil');
const transferSummaryPropMap = require('../constants/transferSummaryPropMap'); const transferSummaryPropMap = require('../constants/transferSummaryPropMap');
const transferSummaryFetchOptions = Array const transferSummaryMethodCallConfig = methodCallUtil
.from(transferSummaryPropMap.keys()) .getMethodCallConfigFromPropMap(transferSummaryPropMap);
.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 processData = (opts, callback, data, error) => { const processData = (opts, callback, data, error) => {
if (error) { if (error) {
@@ -159,7 +144,7 @@ class HistoryService extends EventEmitter {
} }
clientRequestService clientRequestService
.fetchTransferSummary(transferSummaryFetchOptions) .fetchTransferSummary(transferSummaryMethodCallConfig)
.then(this.handleFetchTransferSummarySuccess.bind(this)) .then(this.handleFetchTransferSummarySuccess.bind(this))
.catch(this.handleFetchTransferSummaryError.bind(this)); .catch(this.handleFetchTransferSummaryError.bind(this));
} }

View File

@@ -5,30 +5,15 @@ const config = require('../../config.js');
const clientRequestService = require('./clientRequestService.js'); const clientRequestService = require('./clientRequestService.js');
const clientRequestServiceEvents = require('../constants/clientRequestServiceEvents'); const clientRequestServiceEvents = require('../constants/clientRequestServiceEvents');
const formatUtil = require('../../shared/util/formatUtil'); const formatUtil = require('../../shared/util/formatUtil');
const methodCallUtil = require('../util/methodCallUtil');
const notificationService = require('./notificationService.js'); const notificationService = require('./notificationService.js');
const serverEventTypes = require('../../shared/constants/serverEventTypes'); const serverEventTypes = require('../../shared/constants/serverEventTypes');
const torrentListPropMap = require('../constants/torrentListPropMap'); const torrentListPropMap = require('../constants/torrentListPropMap');
const torrentServiceEvents = require('../constants/torrentServiceEvents.js'); const torrentServiceEvents = require('../constants/torrentServiceEvents.js');
const torrentStatusMap = require('../../shared/constants/torrentStatusMap'); const torrentStatusMap = require('../../shared/constants/torrentStatusMap');
const torrentListFetchOptions = Array const torrentListMethodCallConfig = methodCallUtil
.from(torrentListPropMap.keys()) .getMethodCallConfigFromPropMap(torrentListPropMap);
.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: []
}
);
class TorrentService extends EventEmitter { class TorrentService extends EventEmitter {
constructor() { constructor() {
@@ -40,6 +25,7 @@ class TorrentService extends EventEmitter {
this.fetchTorrentList = this.fetchTorrentList.bind(this); this.fetchTorrentList = this.fetchTorrentList.bind(this);
this.handleTorrentProcessed = this.handleTorrentProcessed.bind(this); this.handleTorrentProcessed = this.handleTorrentProcessed.bind(this);
this.handleTorrentsRemoved = this.handleTorrentsRemoved.bind(this);
clientRequestService.addTorrentListReducer({ clientRequestService.addTorrentListReducer({
key: 'status', key: 'status',
@@ -61,6 +47,11 @@ class TorrentService extends EventEmitter {
this.handleTorrentProcessed this.handleTorrentProcessed
); );
clientRequestService.on(
clientRequestServiceEvents.TORRENTS_REMOVED,
this.handleTorrentsRemoved
);
this.fetchTorrentList(); this.fetchTorrentList();
} }
@@ -117,7 +108,7 @@ class TorrentService extends EventEmitter {
} }
clientRequestService clientRequestService
.fetchTorrentList(torrentListFetchOptions) .fetchTorrentList(torrentListMethodCallConfig)
.then(this.handleFetchTorrentListSuccess.bind(this)) .then(this.handleFetchTorrentListSuccess.bind(this))
.catch(this.handleFetchTorrentListError.bind(this)); .catch(this.handleFetchTorrentListError.bind(this));
} }
@@ -321,6 +312,10 @@ class TorrentService extends EventEmitter {
&& nextData.percentComplete === 100 && nextData.percentComplete === 100
); );
} }
handleTorrentsRemoved() {
this.fetchTorrentList();
}
} }
module.exports = new TorrentService(); module.exports = new TorrentService();

View File

@@ -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;