Skip to content

Commit 96c754d

Browse files
committed
server: rTorrent: save torrents and then add them with paths
Bug: jesec#164, jesec#741, jesec#773
1 parent 6386070 commit 96c754d

File tree

6 files changed

+100
-38
lines changed

6 files changed

+100
-38
lines changed

fixtures/multi.torrent

1.6 KB
Binary file not shown.

fixtures/single.torrent

74.2 KB
Binary file not shown.

server/.jest/auth.setup.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ process.argv = ['node', 'flood'];
99
process.argv.push('--rundir', temporaryRuntimeDirectory);
1010
process.argv.push('--noauth', 'false');
1111

12-
afterAll(() => fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true}));
12+
afterAll(() => {
13+
// TODO: This leads test flakiness caused by ENOENT error
14+
// NeDB provides no method to close database connection
15+
fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true});
16+
});

server/.jest/test.setup.js

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ process.argv.push('--rtsocket', rTorrentSocket);
3737

3838
afterAll((done) => {
3939
rTorrentProcess.on('close', () => {
40+
// TODO: This leads test flakiness caused by ENOENT error
41+
// NeDB provides no method to close database connection
4042
fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true});
4143
done();
4244
});

server/routes/api/torrents.test.ts

+74-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import crypto from 'crypto';
22
import fs from 'fs';
3+
import path from 'path';
34
import readline from 'readline';
45
import stream from 'stream';
56
import supertest from 'supertest';
67

78
import app from '../../app';
89
import {getAuthToken} from './auth';
910
import {getTempPath} from '../../models/TemporaryStorage';
11+
import paths from '../../../shared/config/paths';
1012

11-
import type {AddTorrentByURLOptions, SetTorrentsTrackersOptions} from '../../../shared/types/api/torrents';
13+
import type {
14+
AddTorrentByFileOptions,
15+
AddTorrentByURLOptions,
16+
SetTorrentsTrackersOptions,
17+
} from '../../../shared/types/api/torrents';
1218
import type {TorrentContent} from '../../../shared/types/TorrentContent';
1319
import type {TorrentList, TorrentProperties} from '../../../shared/types/Torrent';
1420
import type {TorrentStatus} from '../../../shared/constants/torrentStatusMap';
@@ -24,6 +30,13 @@ fs.mkdirSync(tempDirectory, {recursive: true});
2430

2531
jest.setTimeout(20000);
2632

33+
const torrentFiles = [
34+
path.join(paths.appSrc, 'fixtures/single.torrent'),
35+
path.join(paths.appSrc, 'fixtures/multi.torrent'),
36+
].map((torrentPath) => Buffer.from(fs.readFileSync(torrentPath)).toString('base64'));
37+
38+
const torrentURLs = ['https://releases.ubuntu.com/20.04/ubuntu-20.04.1-live-server-amd64.iso.torrent'];
39+
2740
let torrentHash = '';
2841

2942
const activityStream = new stream.PassThrough();
@@ -32,7 +45,7 @@ request.get('/api/activity-stream').send().set('Cookie', [authToken]).pipe(activ
3245

3346
describe('POST /api/torrents/add-urls', () => {
3447
const addTorrentByURLOptions: AddTorrentByURLOptions = {
35-
urls: ['https://releases.ubuntu.com/20.04/ubuntu-20.04.1-live-server-amd64.iso.torrent'],
48+
urls: torrentURLs,
3649
destination: tempDirectory,
3750
tags: ['test'],
3851
isBasePath: false,
@@ -41,7 +54,7 @@ describe('POST /api/torrents/add-urls', () => {
4154

4255
const torrentAdded = new Promise((resolve) => {
4356
rl.on('line', (input) => {
44-
if (input.includes('TORRENT_LIST_DIFF_CHANGE')) {
57+
if (input.includes('TORRENT_LIST_ACTION_TORRENT_ADDED')) {
4558
resolve();
4659
}
4760
});
@@ -62,7 +75,7 @@ describe('POST /api/torrents/add-urls', () => {
6275
});
6376
});
6477

65-
it('GET /api/torrents', (done) => {
78+
it('GET /api/torrents to verify torrents are added via URLs', (done) => {
6679
torrentAdded.then(() => {
6780
request
6881
.get('/api/torrents')
@@ -95,6 +108,63 @@ describe('POST /api/torrents/add-urls', () => {
95108
});
96109
});
97110

111+
describe('POST /api/torrents/add-files', () => {
112+
const addTorrentByFileOptions: AddTorrentByFileOptions = {
113+
files: torrentFiles,
114+
destination: tempDirectory,
115+
tags: ['test'],
116+
isBasePath: false,
117+
start: false,
118+
};
119+
120+
const torrentAdded = new Promise((resolve) => {
121+
rl.on('line', (input) => {
122+
if (input.includes('TORRENT_LIST_ACTION_TORRENT_ADDED')) {
123+
resolve();
124+
}
125+
});
126+
});
127+
128+
it('Adds a torrent from files', (done) => {
129+
request
130+
.post('/api/torrents/add-files')
131+
.send(addTorrentByFileOptions)
132+
.set('Cookie', [authToken])
133+
.set('Accept', 'application/json')
134+
.expect(200)
135+
.expect('Content-Type', /json/)
136+
.end((err, _res) => {
137+
if (err) done(err);
138+
139+
done();
140+
});
141+
});
142+
143+
it('GET /api/torrents to verify torrents are added via files', (done) => {
144+
torrentAdded.then(() => {
145+
request
146+
.get('/api/torrents')
147+
.send()
148+
.set('Cookie', [authToken])
149+
.set('Accept', 'application/json')
150+
.expect(200)
151+
.expect('Content-Type', /json/)
152+
.end((err, res) => {
153+
if (err) done(err);
154+
155+
expect(res.body.torrents == null).toBe(false);
156+
const torrentList: TorrentList = res.body.torrents;
157+
158+
expect(Object.keys(torrentList).length).toBeGreaterThanOrEqual(
159+
torrentFiles.length + (torrentHash !== '' ? 1 : 0),
160+
);
161+
162+
done();
163+
});
164+
});
165+
});
166+
});
167+
98168
describe('PATCH /api/torrents/trackers', () => {
99169
const testTrackers = [
100170
`https://${crypto.randomBytes(8).toString('hex')}.com/announce`,

server/services/rTorrent/clientGatewayService.ts

+19-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import path from 'path';
1+
import crypto from 'crypto';
22
import fs from 'fs';
33
import geoip from 'geoip-country';
44
import {moveSync} from 'fs-extra';
5+
import path from 'path';
56
import sanitize from 'sanitize-filename';
67

78
import type {ClientSettings} from '@shared/types/ClientSettings';
@@ -31,6 +32,7 @@ import ClientGatewayService from '../interfaces/clientGatewayService';
3132
import ClientRequestManager from './clientRequestManager';
3233
import scgiUtil from './util/scgiUtil';
3334
import {getMethodCalls, processMethodCallResponse} from './util/rTorrentMethodCallUtil';
35+
import {getTempPath} from '../../models/TemporaryStorage';
3436
import torrentFileUtil from '../../util/torrentFileUtil';
3537
import {
3638
encodeTags,
@@ -55,41 +57,25 @@ class RTorrentClientGatewayService extends ClientGatewayService {
5557
clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings);
5658

5759
async addTorrentsByFile({files, destination, tags, isBasePath, start}: AddTorrentByFileOptions): Promise<void> {
58-
const destinationPath = sanitizePath(destination);
59-
60-
if (!isAllowedPath(destinationPath)) {
61-
throw accessDeniedError();
62-
}
63-
64-
await createDirectory(destinationPath);
65-
66-
// Each torrent is sent individually because rTorrent might have small
67-
// XMLRPC request size limit. This allows the user to send files reliably.
68-
await Promise.all(
69-
files.map(async (file) => {
70-
const additionalCalls: Array<string> = [];
71-
72-
additionalCalls.push(`${isBasePath ? 'd.directory_base.set' : 'd.directory.set'}="${destinationPath}"`);
73-
74-
if (Array.isArray(tags)) {
75-
additionalCalls.push(`d.custom1.set=${encodeTags(tags)}`);
76-
}
60+
const tempPath = path.join(
61+
getTempPath(this.user._id),
62+
'torrents',
63+
`${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
64+
);
65+
await createDirectory(tempPath);
7766

78-
additionalCalls.push(`d.custom.set=addtime,${Date.now() / 1000}`);
79-
80-
return (
81-
this.clientRequestManager
82-
.methodCall(
83-
start ? 'load.raw_start' : 'load.raw',
84-
['', Buffer.from(file, 'base64')].concat(additionalCalls),
85-
)
86-
.then(this.processClientRequestSuccess, this.processClientRequestError)
87-
.then(() => {
88-
// returns nothing.
89-
}) || Promise.reject()
90-
);
67+
const torrentPaths = await Promise.all(
68+
files.map(async (file, index) => {
69+
const torrentPath = path.join(tempPath, `${index}.torrent`);
70+
fs.writeFileSync(torrentPath, Buffer.from(file, 'base64'), {});
71+
return torrentPath;
9172
}),
9273
);
74+
75+
// Delete temp files after 5 minutes. This is more than enough.
76+
setTimeout(() => fs.rmdirSync(tempPath, {recursive: true}), 1000 * 60 * 5);
77+
78+
return this.addTorrentsByURL({urls: torrentPaths, destination, tags, isBasePath, start});
9379
}
9480

9581
async addTorrentsByURL({urls, destination, tags, isBasePath, start}: AddTorrentByURLOptions): Promise<void> {

0 commit comments

Comments
 (0)