AddTorrentByFile: use base64 instead of Blob to transfer file

This commit is contained in:
Jesse Chan
2020-09-29 22:17:59 +08:00
parent b0bd3c1b6e
commit c6f8b9b76d
11 changed files with 127 additions and 208 deletions
@@ -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,
},
});
},
@@ -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<string, unknown>;
files: Array<{
name: string;
data: string;
}>;
tags: string;
isAddingTorrents: boolean;
}
class AddTorrentsByFile extends React.Component<WrappedComponentProps, AddTorrentsByFileStates> {
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) => (
<li className="dropzone__selected-files__file interactive-list__item" key={file.name} title={file.name}>
<span className="interactive-list__icon">
<File />
<FileIcon />
</span>
<span className="interactive-list__label">{file.name}</span>
<span
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
onClick={() => this.handleFileRemove(index)}>
<Close />
<CloseIcon />
</span>
</li>
));
fileContent = (
<ul className="dropzone__selected-files interactive-list" onClick={this.handleFilesClick}>
<ul
className="dropzone__selected-files interactive-list"
onClick={(event) => {
event.stopPropagation();
}}>
{files}
</ul>
);
@@ -63,7 +84,7 @@ class AddTorrentsByFile extends React.Component {
<input {...getInputProps()} />
<div className="dropzone__copy">
<div className="dropzone__icon">
<Files />
<FilesIcon />
</div>
<FormattedMessage id="torrents.add.tab.file.drop" />{' '}
<span className="dropzone__browse-button">
@@ -78,47 +99,69 @@ class AddTorrentsByFile extends React.Component {
);
}
handleFileDrop = (files) => {
handleFileDrop = (files: Array<File>) => {
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<AddTorrentsByFileFormData>;
const filesData: Array<string> = [];
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() {
@@ -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 = () => {
-109
View File
@@ -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",
-2
View File
@@ -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",
+2 -2
View File
@@ -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);
-11
View File
@@ -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();
};
+4 -11
View File
@@ -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));
});
}
+3 -13
View File
@@ -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,
+2 -10
View File
@@ -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) => {
+14
View File
@@ -9,6 +9,20 @@ export interface AddTorrentByURLOptions {
tags?: Array<string>;
}
// POST /api/client/add-files
export interface AddTorrentByFileOptions {
// Torrent files in base64
files: Array<string>;
// Path of destination
destination: string;
// Tags
tags?: Array<string>;
// 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