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_START',
'PROCESS_TRANSFER_RATE_START',
'TORRENTS_REMOVED'
];
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) {
let hashes = this.getEnsuredArray(options.hashes);

View File

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

View File

@@ -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) {

View File

@@ -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) {

View File

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

View File

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

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;