From 0df02681952e238534add394c356acf0cae34cd9 Mon Sep 17 00:00:00 2001 From: John Furrow Date: Tue, 5 Jul 2016 20:32:29 -0700 Subject: [PATCH 1/2] Allow deleting data on torrent removal --- client/scripts/actions/TorrentActions.js | 7 +- .../Modals/RemoveTorrentsModal/index.js | 140 ++++++++++++++++++ client/scripts/components/Modals/index.js | 2 + .../components/TorrentList/ActionBar.js | 65 +------- client/scripts/i18n/en.js | 1 + client/scripts/stores/TorrentStore.js | 5 + package.json | 1 + server/models/ClientRequest.js | 1 - server/models/Torrent.js | 4 + server/models/client.js | 31 +++- server/routes/client.js | 5 +- 11 files changed, 189 insertions(+), 73 deletions(-) create mode 100644 client/scripts/components/Modals/RemoveTorrentsModal/index.js diff --git a/client/scripts/actions/TorrentActions.js b/client/scripts/actions/TorrentActions.js index fc81017a..351a78c3 100644 --- a/client/scripts/actions/TorrentActions.js +++ b/client/scripts/actions/TorrentActions.js @@ -54,8 +54,8 @@ const TorrentActions = { }); }, - deleteTorrents: (hash) => { - return axios.post('/api/client/torrents/delete', {hash}) + deleteTorrents: (hash, deleteData) => { + return axios.post('/api/client/torrents/delete', {hash, deleteData}) .then((json = {}) => { return json.data; }) @@ -64,7 +64,8 @@ const TorrentActions = { type: ActionTypes.CLIENT_REMOVE_TORRENT_SUCCESS, data: { data, - count: hash.length + count: hash.length, + deleteData } }); }) diff --git a/client/scripts/components/Modals/RemoveTorrentsModal/index.js b/client/scripts/components/Modals/RemoveTorrentsModal/index.js new file mode 100644 index 00000000..a62bae09 --- /dev/null +++ b/client/scripts/components/Modals/RemoveTorrentsModal/index.js @@ -0,0 +1,140 @@ +import _ from 'lodash'; +import classnames from 'classnames'; +import {formatMessage, FormattedMessage, injectIntl} from 'react-intl'; +import React from 'react'; + +import Checkbox from '../../General/FormElements/Checkbox'; +import Modal from '../Modal'; +import SettingsStore from '../../../stores/SettingsStore'; +import stringUtil from '../../../../../shared/util/stringUtil'; +import TorrentActions from '../../../actions/TorrentActions'; +import TorrentStore from '../../../stores/TorrentStore'; + +const METHODS_TO_BIND = [ + 'handleRemovalConfirmation', + 'handleCheckboxChange' +]; + +class RemoveTorrentsModal extends React.Component { + constructor() { + super(); + + this.state = { + deleteData: SettingsStore.getFloodSettings('deleteTorrentData') + }; + + METHODS_TO_BIND.forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + getActions(torrents) { + if (torrents.length === 0) { + return [ + { + clickHandler: null, + content: 'OK', + triggerDismiss: true, + type: 'primary' + } + ]; + } + + return [ + { + clickHandler: this.handleRemoveTorrentDecline, + content: this.props.intl.formatMessage({ + id: 'button.no', + defaultMessage: 'No' + }), + triggerDismiss: true, + type: 'secondary' + }, + { + clickHandler: this.handleRemovalConfirmation.bind(this, torrents), + content: this.props.intl.formatMessage({ + id: 'button.yes', + defaultMessage: 'Yes' + }), + triggerDismiss: true, + type: 'primary' + } + ]; + } + + getContent(torrents) { + let modalContent = null; + let deleteDataContent = null; + let selectedTorrentCount = torrents.length; + + if (selectedTorrentCount === 0) { + modalContent = this.props.intl.formatMessage({ + id: 'torrents.remove.error.no.torrents.selected', + defaultMessage: 'You haven\'t selected any torrents.' + }); + } else { + let torrentText = stringUtil.pluralize('torrent', selectedTorrentCount); + modalContent = this.props.intl.formatMessage({ + id: 'torrents.remove.are.you.sure', + defaultMessage: `Are you sure you want to remove {count, plural, + =0 {no torrents} + =1 {one torrent} + other {# torrents} + }?` + }, { + count: selectedTorrentCount + }); + + deleteDataContent = ( +
+
+ + + +
+
+ ); + } + + return ( +
+
+
+ {modalContent} +
+
+ {deleteDataContent} +
+ ); + } + + handleCheckboxChange(checkboxState) { + this.setState({deleteData: checkboxState}); + } + + handleRemovalConfirmation(torrents) { + TorrentActions.deleteTorrents(torrents, this.state.deleteData); + } + + render() { + let selectedTorrents = TorrentStore.getSelectedTorrents() || []; + let modalHeading = this.props.intl.formatMessage({ + id: 'torrents.remove', + defaultMessage: 'Remove Torrents' + }); + + return ( + + ); + } +} + +export default injectIntl(RemoveTorrentsModal); diff --git a/client/scripts/components/Modals/index.js b/client/scripts/components/Modals/index.js index 911e9846..fb3fc1d9 100644 --- a/client/scripts/components/Modals/index.js +++ b/client/scripts/components/Modals/index.js @@ -7,6 +7,7 @@ import ConfirmModal from './ConfirmModal'; import EventTypes from '../../constants/EventTypes'; import Modal from './Modal'; import MoveTorrentsModal from './MoveTorrentsModal'; +import RemoveTorrentsModal from './RemoveTorrentsModal'; import SetTagsModal from './SetTagsModal'; import SettingsModal from './SettingsModal'; import TorrentDetailsModal from './TorrentDetailsModal'; @@ -27,6 +28,7 @@ export default class Modals extends React.Component { 'add-torrents': AddTorrentsModal, confirm: ConfirmModal, 'move-torrents': MoveTorrentsModal, + 'remove-torrents': RemoveTorrentsModal, 'set-taxonomy': SetTagsModal, settings: SettingsModal, 'torrent-details': TorrentDetailsModal diff --git a/client/scripts/components/TorrentList/ActionBar.js b/client/scripts/components/TorrentList/ActionBar.js index aced09a3..a944269f 100644 --- a/client/scripts/components/TorrentList/ActionBar.js +++ b/client/scripts/components/TorrentList/ActionBar.js @@ -51,71 +51,9 @@ class ActionBar extends React.Component { UIActions.displayModal({id: 'add-torrents'}); } - handleRemoveTorrentConfirm(torrents) { - TorrentActions.deleteTorrents(torrents); - } - handleRemoveTorrents() { - let selectedTorrents = TorrentStore.getSelectedTorrents() || []; - let selectedTorrentCount = selectedTorrents.length; - - let actions = [ - { - clickHandler: this.handleRemoveTorrentDecline, - content: this.props.intl.formatMessage({ - id: 'button.no', - defaultMessage: 'No' - }), - triggerDismiss: true, - type: 'secondary' - }, - { - clickHandler: this.handleRemoveTorrentConfirm.bind(this, selectedTorrents), - content: this.props.intl.formatMessage({ - id: 'button.yes', - defaultMessage: 'Yes' - }), - triggerDismiss: true, - type: 'primary' - } - ]; - - let content = this.props.intl.formatMessage({ - id: 'torrents.remove.are.you.sure', - defaultMessage: `Are you sure you want to remove {count, plural, - =0 {no torrents} - =1 {one torrent} - other {# torrents} - }?` - }, { - count: selectedTorrentCount - }); - - if (selectedTorrentCount === 0) { - actions = [ - { - clickHandler: null, - content: 'OK', - triggerDismiss: true, - type: 'primary' - } - ]; - content = this.props.intl.formatMessage({ - id: 'torrents.remove.error.no.torrents.selected', - defaultMessage: 'You haven\'t selected any torrents.' - }); - } - UIActions.displayModal({ - id: 'confirm', - options: { - actions, - content, - heading: this.props.intl.formatMessage({ - id: 'torrents.remove', - defaultMessage: 'Remove Torrents' - }) - } + id: 'remove-torrents' }); } @@ -165,7 +103,6 @@ class ActionBar extends React.Component { ); } - } export default injectIntl(ActionBar); diff --git a/client/scripts/i18n/en.js b/client/scripts/i18n/en.js index 149180d3..f3ef9058 100644 --- a/client/scripts/i18n/en.js +++ b/client/scripts/i18n/en.js @@ -196,6 +196,7 @@ export default { =1 {one torrent} other {# torrents} }?`, + 'torrents.remove.delete.data': 'Delete data', 'torrents.remove.error.no.torrents.selected': 'You haven\'t selected any torrents.', 'torrents.remove': 'Remove Torrents' }; diff --git a/client/scripts/stores/TorrentStore.js b/client/scripts/stores/TorrentStore.js index 4531e4f5..6f53540e 100644 --- a/client/scripts/stores/TorrentStore.js +++ b/client/scripts/stores/TorrentStore.js @@ -198,6 +198,11 @@ class TorrentStoreClass extends BaseStore { } handleRemoveTorrentsSuccess(response) { + SettingsStore.saveFloodSettings({ + id: 'deleteTorrentData', + data: response.deleteData + }); + NotificationStore.add({ accumulation: { id: 'notification.torrent.remove', diff --git a/package.json b/package.json index 96f314cc..7b4b971d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "cookie-parser": "^1.4.3", "d3": "^3.5.6", "debug": "^2.2.0", + "del": "^2.2.1", "es6-promise": "^3.2.1", "events": "^1.1.0", "express": "^4.14.0", diff --git a/server/models/ClientRequest.js b/server/models/ClientRequest.js index 9ceff3ff..39b8eacb 100644 --- a/server/models/ClientRequest.js +++ b/server/models/ClientRequest.js @@ -1,6 +1,5 @@ 'use strict'; -let fs = require('fs'); let mv = require('mv'); let path = require('path'); let util = require('util'); diff --git a/server/models/Torrent.js b/server/models/Torrent.js index 9198c1f0..1ce5a1f0 100644 --- a/server/models/Torrent.js +++ b/server/models/Torrent.js @@ -61,6 +61,10 @@ class Torrent { this._torrentData = this.getCalculatedClientData(clientData, opts); } + get basePath() { + return this._torrentData.basePath; + } + get data() { // TODO: Only return the properties that are different than the last time // get was called. Perhaps identify the last time it was called by ID so diff --git a/server/models/client.js b/server/models/client.js index d042c806..3503fd5f 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -1,6 +1,6 @@ 'use strict'; -let fs = require('fs'); +let del = require('del'); let util = require('util'); let clientResponseUtil = require('../util/clientResponseUtil'); @@ -64,11 +64,34 @@ var client = { request.send(); }, - deleteTorrents: (hashes, callback) => { + deleteTorrents: (options, callback) => { + let files = []; let request = new ClientRequest(); - request.add('removeTorrents', {hashes}); - request.onComplete(callback); + if (options.deleteData) { + let torrents = torrentCollection.torrents; + + files = options.hashes.reduce((memo, hash) => { + let filePath = torrents[hash].basePath; + + // Let's not try to delete these files. + if (filePath != null && filePath !== '/' && filePath !== '' + && filePath !== '.') { + memo.push(filePath); + } + + return memo; + }, []); + } + + request.add('removeTorrents', {hashes: options.hashes}); + request.onComplete((response, error) => { + if (options.deleteData && files.length > 0) { + del(files, {force: true}); + } + + callback(response, error); + }); request.send(); }, diff --git a/server/routes/client.js b/server/routes/client.js index 6f0b00ff..4db6562a 100644 --- a/server/routes/client.js +++ b/server/routes/client.js @@ -69,7 +69,10 @@ router.post('/torrents/move', function(req, res, next) { }); router.post('/torrents/delete', function(req, res, next) { - client.deleteTorrents(req.body.hash, ajaxUtil.getResponseFn(res)); + let deleteData = req.body.deleteData; + let hashes = req.body.hash; + + client.deleteTorrents({hashes, deleteData}, ajaxUtil.getResponseFn(res)); }); router.get('/torrents/taxonomy', function(req, res, next) { From dd164b5d67cda26382e9d583c427744deb458d28 Mon Sep 17 00:00:00 2001 From: John Furrow Date: Wed, 13 Jul 2016 21:59:18 -0700 Subject: [PATCH 2/2] Display Remove modal from context menu --- client/scripts/components/TorrentList/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/scripts/components/TorrentList/index.js b/client/scripts/components/TorrentList/index.js index 8b9d5d0b..f97d023e 100644 --- a/client/scripts/components/TorrentList/index.js +++ b/client/scripts/components/TorrentList/index.js @@ -186,7 +186,7 @@ class TorrentListContainer extends React.Component { TorrentActions.pauseTorrents(selectedTorrents); break; case 'remove': - TorrentActions.deleteTorrents(selectedTorrents); + UIActions.displayModal({id: 'remove-torrents'}); break; case 'move': UIActions.displayModal({id: 'move-torrents'});