From c6f8b9b76d1438eeaf9423f1667f4aff9bebec54 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Tue, 29 Sep 2020 22:17:59 +0800 Subject: [PATCH] AddTorrentByFile: use base64 instead of Blob to transfer file --- .../src/javascript/actions/TorrentActions.ts | 13 ++- ...orrentsByFile.js => AddTorrentsByFile.tsx} | 103 ++++++++++++----- .../components/torrent-list/TorrentList.js | 36 +++--- package-lock.json | 109 ------------------ package.json | 2 - server/app.ts | 4 +- server/middleware/booleanCoerce.ts | 11 -- server/models/ClientRequest.js | 15 +-- server/models/client.js | 16 +-- server/routes/client.ts | 12 +- shared/types/Action.ts | 14 +++ 11 files changed, 127 insertions(+), 208 deletions(-) rename client/src/javascript/components/modals/add-torrents-modal/{AddTorrentsByFile.js => AddTorrentsByFile.tsx} (61%) delete mode 100644 server/middleware/booleanCoerce.ts diff --git a/client/src/javascript/actions/TorrentActions.ts b/client/src/javascript/actions/TorrentActions.ts index 00653830..3594eea3 100644 --- a/client/src/javascript/actions/TorrentActions.ts +++ b/client/src/javascript/actions/TorrentActions.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import type { + AddTorrentByFileOptions, AddTorrentByURLOptions, CheckTorrentsOptions, DeleteTorrentsOptions, @@ -41,18 +42,18 @@ const TorrentActions = { }, ), - addTorrentsByFiles: (formData: FormData, destination: string) => + addTorrentsByFiles: (options: AddTorrentByFileOptions) => axios - .post(`${baseURI}api/client/add-files`, formData) + .post(`${baseURI}api/client/add-files`, options) .then((json) => json.data) .then( - (response) => { + (data) => { AppDispatcher.dispatchServerAction({ type: 'CLIENT_ADD_TORRENT_SUCCESS', data: { - count: formData.getAll('torrents').length, - destination, - response, + count: options.files.length, + destination: options.destination, + data, }, }); }, diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx similarity index 61% rename from client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js rename to client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx index 0e06cb63..60a5af84 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx @@ -1,26 +1,43 @@ -import {FormattedMessage, injectIntl} from 'react-intl'; +import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl'; import Dropzone from 'react-dropzone'; import React from 'react'; import {Form, FormRow, FormRowItem, Textbox} from '../../../ui'; import AddTorrentsActions from './AddTorrentsActions'; -import Close from '../../icons/Close'; -import File from '../../icons/File'; -import Files from '../../icons/Files'; +import CloseIcon from '../../icons/Close'; +import FileIcon from '../../icons/File'; +import FilesIcon from '../../icons/Files'; import SettingsStore from '../../../stores/SettingsStore'; import TorrentActions from '../../../actions/TorrentActions'; import TorrentDestination from '../../general/filesystem/TorrentDestination'; -class AddTorrentsByFile extends React.Component { - formRef = null; +interface AddTorrentsByFileFormData { + destination: string; + start: boolean; + tags: string; + isBasePath: boolean; +} - constructor(props) { +interface AddTorrentsByFileStates { + errors: Record; + files: Array<{ + name: string; + data: string; + }>; + tags: string; + isAddingTorrents: boolean; +} + +class AddTorrentsByFile extends React.Component { + formRef: Form | null = null; + + constructor(props: WrappedComponentProps) { super(props); this.state = { errors: {}, - isAddingTorrents: false, files: [], tags: '', + isAddingTorrents: false, }; } @@ -31,19 +48,23 @@ class AddTorrentsByFile extends React.Component { const files = this.state.files.map((file, index) => (
  • - + {file.name} this.handleFileRemove(index)}> - +
  • )); fileContent = ( -
      +
        { + event.stopPropagation(); + }}> {files}
      ); @@ -63,7 +84,7 @@ class AddTorrentsByFile extends React.Component {
      - +
      {' '} @@ -78,47 +99,69 @@ class AddTorrentsByFile extends React.Component { ); } - handleFileDrop = (files) => { + handleFileDrop = (files: Array) => { const nextErrorsState = this.state.errors; if (nextErrorsState.files != null) { delete nextErrorsState.files; } - this.setState((state) => ({errors: nextErrorsState, files: state.files.concat(files)})); + files.forEach((file) => { + const reader = new FileReader(); + reader.onload = (e) => { + this.setState((state) => { + if (e.target != null && e.target.result != null && typeof e.target.result === 'string') { + return { + errors: nextErrorsState, + files: state.files.concat({ + name: file.name, + data: e.target.result.split('base64,')[1], + }), + }; + } + return {errors: nextErrorsState, files: state.files}; + }); + }; + reader.readAsDataURL(file); + }); }; - handleFileRemove = (fileIndex) => { + handleFileRemove = (fileIndex: number) => { const {files} = this.state; files.splice(fileIndex, 1); this.setState({files}); }; - handleFilesClick(event) { - event.stopPropagation(); - } - handleAddTorrents = () => { + if (this.formRef == null) { + return; + } + const formData = this.formRef.getFormData(); this.setState({isAddingTorrents: true}); - const fileData = new FormData(); - const {destination, start, tags, isBasePath} = formData; + const {destination, start, tags, isBasePath} = formData as Partial; + const filesData: Array = []; this.state.files.forEach((file) => { - fileData.append('torrents', file); + filesData.push(file.data); }); - tags.split(',').forEach((tag) => { - fileData.append('tags', tag); + if (filesData.length === 0 || destination == null) { + return; + } + + TorrentActions.addTorrentsByFiles({ + files: filesData, + destination, + tags: tags != null ? tags.split(',') : undefined, + isBasePath: isBasePath || false, + start: start || false, }); - fileData.append('destination', destination); - fileData.append('isBasePath', isBasePath); - fileData.append('start', start); - - TorrentActions.addTorrentsByFiles(fileData, destination); - SettingsStore.setFloodSetting('startTorrentsOnLoad', start); + if (start != null) { + SettingsStore.setFloodSetting('startTorrentsOnLoad', start); + } }; render() { diff --git a/client/src/javascript/components/torrent-list/TorrentList.js b/client/src/javascript/components/torrent-list/TorrentList.js index e53c7b72..053ffbfb 100644 --- a/client/src/javascript/components/torrent-list/TorrentList.js +++ b/client/src/javascript/components/torrent-list/TorrentList.js @@ -95,25 +95,33 @@ class TorrentListContainer extends React.Component { }; handleFileDrop = (files) => { - const destination = - SettingsStore.getFloodSetting('torrentDestination') || SettingsStore.getClientSetting('directoryDefault') || ''; + const filesData = []; - const isBasePath = false; + const callback = (data) => { + filesData.concat(data); - const start = SettingsStore.getFloodSetting('startTorrentsOnLoad'); - - const fileData = new FormData(); + if (filesData.length === files.length) { + TorrentActions.addTorrentsByFiles({ + files: filesData, + destination: + SettingsStore.getFloodSetting('torrentDestination') || + SettingsStore.getClientSetting('directoryDefault') || + '', + isBasePath: false, + start: SettingsStore.getFloodSetting('startTorrentsOnLoad'), + }); + } + }; files.forEach((file) => { - fileData.append('torrents', file); + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target != null && e.target.result != null && typeof e.target.result === 'string') { + callback(e.target.result.split('base64,')[1]); + } + }; + reader.readAsDataURL(file); }); - - fileData.append('destination', destination); - fileData.append('isBasePath', isBasePath); - fileData.append('start', start); - fileData.append('tags', ''); - - TorrentActions.addTorrentsByFiles(fileData, destination); }; handleTorrentFilterChange = () => { diff --git a/package-lock.json b/package-lock.json index 1debc34b..54552b9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1988,15 +1988,6 @@ "@types/node": "*" } }, - "@types/multer": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz", - "integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==", - "dev": true, - "requires": { - "@types/express": "*" - } - }, "@types/nedb": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/@types/nedb/-/nedb-1.8.11.tgz", @@ -2810,12 +2801,6 @@ "picomatch": "^2.0.4" } }, - "append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=", - "dev": true - }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -3943,42 +3928,6 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, - "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", - "dev": true, - "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -6198,42 +6147,6 @@ } } }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "dev": true, - "requires": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -11210,22 +11123,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "multer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", - "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", - "dev": true, - "requires": { - "append-field": "^1.0.0", - "busboy": "^0.2.11", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.1", - "object-assign": "^4.1.1", - "on-finished": "^2.3.0", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - } - }, "multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", @@ -16959,12 +16856,6 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", - "dev": true - }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", diff --git a/package.json b/package.json index ecd3b912..1a10a548 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "@types/http-errors": "^1.8.0", "@types/jsonwebtoken": "^8.5.0", "@types/morgan": "^1.9.1", - "@types/multer": "^1.4.4", "@types/nedb": "^1.8.11", "@types/node": "^14.11.2", "@types/passport": "^1.0.4", @@ -124,7 +123,6 @@ "minami": "^1.2.3", "mini-css-extract-plugin": "^0.11.2", "morgan": "^1.10.0", - "multer": "^1.4.2", "nedb": "^1.8.0", "node-sass": "^4.13.0", "nodemon": "^2.0.4", diff --git a/server/app.ts b/server/app.ts index 00d41d08..6709c63e 100644 --- a/server/app.ts +++ b/server/app.ts @@ -37,8 +37,8 @@ app.set('etag', false); app.use(morgan('dev')); app.use(passport.initialize()); app.use(compression()); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: false})); +app.use(bodyParser.json({limit: '50mb'})); +app.use(bodyParser.urlencoded({extended: false, limit: '50mb'})); app.use(cookieParser()); passportConfig(passport); diff --git a/server/middleware/booleanCoerce.ts b/server/middleware/booleanCoerce.ts deleted file mode 100644 index 3bef0a6c..00000000 --- a/server/middleware/booleanCoerce.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {Request, Response, NextFunction} from 'express'; - -export default (key: string) => (req: Request, _res: Response, next: NextFunction) => { - const value = req.body && req.body[key]; - - if (value && typeof value === 'string') { - req.body[key] = value === 'true'; - } - - next(); -}; diff --git a/server/models/ClientRequest.js b/server/models/ClientRequest.js index 9b1f43df..8fd72b08 100644 --- a/server/models/ClientRequest.js +++ b/server/models/ClientRequest.js @@ -114,13 +114,12 @@ class ClientRequest { // TODO: Separate these and add support for additional clients. // rTorrent method calls. addFiles(options) { - const files = getEnsuredArray(options.files); - const {path: destinationPath, isBasePath, start, tags: tagsArr} = options; + const {files, path: destinationPath, isBasePath, start, tags: tagsArr} = options; files.forEach((file) => { - let methodCall = 'load.raw_start'; - let parameters = ['', file.buffer]; + const methodCall = start ? 'load.raw_start' : 'load.raw'; const timeAdded = Math.floor(Date.now() / 1000); + let parameters = ['', Buffer.from(file, 'base64')]; if (destinationPath) { if (isBasePath) { @@ -132,15 +131,9 @@ class ClientRequest { parameters = addTagsToRequest(tagsArr, parameters); - parameters.push(`d.custom.set=x-filename,${file.originalname}`); + // parameters.push(`d.custom.set=x-filename,${file.originalname}`); parameters.push(`d.custom.set=addtime,${timeAdded}`); - // The start value is a string because it was appended to a FormData - // object. - if (start === 'false') { - methodCall = 'load.raw'; - } - this.requests.push(getMethodCall(methodCall, parameters)); }); } diff --git a/server/models/client.js b/server/models/client.js index 7327cc2f..8fc5e255 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -15,15 +15,8 @@ import torrentFileUtil from '../util/torrentFileUtil'; import torrentTrackerPropsMap from '../../shared/constants/torrentTrackerPropsMap'; const client = { - addFiles(user, services, req, callback) { - const {files} = req; - const {destination: destinationPath, isBasePath, start} = req.body; - let {tags} = req.body; - const request = new ClientRequest(user, services); - - if (!Array.isArray(tags)) { - tags = tags.split(','); - } + addFiles(user, services, options, callback) { + const {destination: destinationPath, files, isBasePath, start, tags} = options; const resolvedPath = fileUtil.sanitizePath(destinationPath); if (!fileUtil.isAllowedPath(resolvedPath)) { @@ -32,17 +25,14 @@ const client = { } fileUtil.createDirectory({path: resolvedPath}); - request.send(); // Each torrent is sent individually because rTorrent accepts a total // filesize of 524 kilobytes or less. This allows the user to send many // torrent files reliably. files.forEach((file, index) => { - file.originalname = encodeURIComponent(file.originalname); - const fileRequest = new ClientRequest(user, services); fileRequest.addFiles({ - files: file, + files: [file], path: resolvedPath, isBasePath, start, diff --git a/server/routes/client.ts b/server/routes/client.ts index 6619c393..7dbbadf2 100644 --- a/server/routes/client.ts +++ b/server/routes/client.ts @@ -1,5 +1,4 @@ import express from 'express'; -import multer from 'multer'; import type { CheckTorrentsOptions, @@ -10,17 +9,10 @@ import type { } from '@shared/types/Action'; import ajaxUtil from '../util/ajaxUtil'; -import booleanCoerce from '../middleware/booleanCoerce'; import client from '../models/client'; const router = express.Router(); -const upload = multer({ - dest: 'uploads/', - limits: {fileSize: 10000000}, - storage: multer.memoryStorage(), -}); - router.get('/connection-test', (req, res) => { req.services?.clientGatewayService .testGateway() @@ -47,8 +39,8 @@ router.post('/add', (req, res) => { client.addUrls(req.user, req.services, req.body, ajaxUtil.getResponseFn(res)); }); -router.post('/add-files', upload.array('torrents'), booleanCoerce('isBasePath'), (req, res) => { - client.addFiles(req.user, req.services, req, ajaxUtil.getResponseFn(res)); +router.post('/add-files', (req, res) => { + client.addFiles(req.user, req.services, req.body, ajaxUtil.getResponseFn(res)); }); router.get('/settings', (req, res) => { diff --git a/shared/types/Action.ts b/shared/types/Action.ts index cd4359fe..b5c6162b 100644 --- a/shared/types/Action.ts +++ b/shared/types/Action.ts @@ -9,6 +9,20 @@ export interface AddTorrentByURLOptions { tags?: Array; } +// POST /api/client/add-files +export interface AddTorrentByFileOptions { + // Torrent files in base64 + files: Array; + // Path of destination + destination: string; + // Tags + tags?: Array; + // Whether destination is the base path + isBasePath: boolean; + // Whether to start torrent + start: boolean; +} + // POST /api/client/torrents/check-hash export interface CheckTorrentsOptions { // An array of string representing hashes of torrents to be checked