mirror of
https://github.com/zoriya/flood.git
synced 2025-12-05 23:06:20 +00:00
server: rTorrent: save torrents and then add them with paths
Bug: #164, #741, #773
This commit is contained in:
BIN
fixtures/multi.torrent
Normal file
BIN
fixtures/multi.torrent
Normal file
Binary file not shown.
BIN
fixtures/single.torrent
Normal file
BIN
fixtures/single.torrent
Normal file
Binary file not shown.
@@ -9,4 +9,8 @@ process.argv = ['node', 'flood'];
|
|||||||
process.argv.push('--rundir', temporaryRuntimeDirectory);
|
process.argv.push('--rundir', temporaryRuntimeDirectory);
|
||||||
process.argv.push('--noauth', 'false');
|
process.argv.push('--noauth', 'false');
|
||||||
|
|
||||||
afterAll(() => fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true}));
|
afterAll(() => {
|
||||||
|
// TODO: This leads test flakiness caused by ENOENT error
|
||||||
|
// NeDB provides no method to close database connection
|
||||||
|
fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true});
|
||||||
|
});
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ process.argv.push('--rtsocket', rTorrentSocket);
|
|||||||
|
|
||||||
afterAll((done) => {
|
afterAll((done) => {
|
||||||
rTorrentProcess.on('close', () => {
|
rTorrentProcess.on('close', () => {
|
||||||
|
// TODO: This leads test flakiness caused by ENOENT error
|
||||||
|
// NeDB provides no method to close database connection
|
||||||
fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true});
|
fs.rmdirSync(temporaryRuntimeDirectory, {recursive: true});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import readline from 'readline';
|
import readline from 'readline';
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
@@ -7,8 +8,13 @@ import supertest from 'supertest';
|
|||||||
import app from '../../app';
|
import app from '../../app';
|
||||||
import {getAuthToken} from './auth';
|
import {getAuthToken} from './auth';
|
||||||
import {getTempPath} from '../../models/TemporaryStorage';
|
import {getTempPath} from '../../models/TemporaryStorage';
|
||||||
|
import paths from '../../../shared/config/paths';
|
||||||
|
|
||||||
import type {AddTorrentByURLOptions, SetTorrentsTrackersOptions} from '../../../shared/types/api/torrents';
|
import type {
|
||||||
|
AddTorrentByFileOptions,
|
||||||
|
AddTorrentByURLOptions,
|
||||||
|
SetTorrentsTrackersOptions,
|
||||||
|
} from '../../../shared/types/api/torrents';
|
||||||
import type {TorrentContent} from '../../../shared/types/TorrentContent';
|
import type {TorrentContent} from '../../../shared/types/TorrentContent';
|
||||||
import type {TorrentList, TorrentProperties} from '../../../shared/types/Torrent';
|
import type {TorrentList, TorrentProperties} from '../../../shared/types/Torrent';
|
||||||
import type {TorrentStatus} from '../../../shared/constants/torrentStatusMap';
|
import type {TorrentStatus} from '../../../shared/constants/torrentStatusMap';
|
||||||
@@ -24,6 +30,13 @@ fs.mkdirSync(tempDirectory, {recursive: true});
|
|||||||
|
|
||||||
jest.setTimeout(20000);
|
jest.setTimeout(20000);
|
||||||
|
|
||||||
|
const torrentFiles = [
|
||||||
|
path.join(paths.appSrc, 'fixtures/single.torrent'),
|
||||||
|
path.join(paths.appSrc, 'fixtures/multi.torrent'),
|
||||||
|
].map((torrentPath) => Buffer.from(fs.readFileSync(torrentPath)).toString('base64'));
|
||||||
|
|
||||||
|
const torrentURLs = ['https://releases.ubuntu.com/20.04/ubuntu-20.04.1-live-server-amd64.iso.torrent'];
|
||||||
|
|
||||||
let torrentHash = '';
|
let torrentHash = '';
|
||||||
|
|
||||||
const activityStream = new stream.PassThrough();
|
const activityStream = new stream.PassThrough();
|
||||||
@@ -32,7 +45,7 @@ request.get('/api/activity-stream').send().set('Cookie', [authToken]).pipe(activ
|
|||||||
|
|
||||||
describe('POST /api/torrents/add-urls', () => {
|
describe('POST /api/torrents/add-urls', () => {
|
||||||
const addTorrentByURLOptions: AddTorrentByURLOptions = {
|
const addTorrentByURLOptions: AddTorrentByURLOptions = {
|
||||||
urls: ['https://releases.ubuntu.com/20.04/ubuntu-20.04.1-live-server-amd64.iso.torrent'],
|
urls: torrentURLs,
|
||||||
destination: tempDirectory,
|
destination: tempDirectory,
|
||||||
tags: ['test'],
|
tags: ['test'],
|
||||||
isBasePath: false,
|
isBasePath: false,
|
||||||
@@ -41,7 +54,7 @@ describe('POST /api/torrents/add-urls', () => {
|
|||||||
|
|
||||||
const torrentAdded = new Promise((resolve) => {
|
const torrentAdded = new Promise((resolve) => {
|
||||||
rl.on('line', (input) => {
|
rl.on('line', (input) => {
|
||||||
if (input.includes('TORRENT_LIST_DIFF_CHANGE')) {
|
if (input.includes('TORRENT_LIST_ACTION_TORRENT_ADDED')) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -62,7 +75,7 @@ describe('POST /api/torrents/add-urls', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('GET /api/torrents', (done) => {
|
it('GET /api/torrents to verify torrents are added via URLs', (done) => {
|
||||||
torrentAdded.then(() => {
|
torrentAdded.then(() => {
|
||||||
request
|
request
|
||||||
.get('/api/torrents')
|
.get('/api/torrents')
|
||||||
@@ -95,6 +108,63 @@ describe('POST /api/torrents/add-urls', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /api/torrents/add-files', () => {
|
||||||
|
const addTorrentByFileOptions: AddTorrentByFileOptions = {
|
||||||
|
files: torrentFiles,
|
||||||
|
destination: tempDirectory,
|
||||||
|
tags: ['test'],
|
||||||
|
isBasePath: false,
|
||||||
|
start: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const torrentAdded = new Promise((resolve) => {
|
||||||
|
rl.on('line', (input) => {
|
||||||
|
if (input.includes('TORRENT_LIST_ACTION_TORRENT_ADDED')) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Adds a torrent from files', (done) => {
|
||||||
|
request
|
||||||
|
.post('/api/torrents/add-files')
|
||||||
|
.send(addTorrentByFileOptions)
|
||||||
|
.set('Cookie', [authToken])
|
||||||
|
.set('Accept', 'application/json')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.end((err, _res) => {
|
||||||
|
if (err) done(err);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /api/torrents to verify torrents are added via files', (done) => {
|
||||||
|
torrentAdded.then(() => {
|
||||||
|
request
|
||||||
|
.get('/api/torrents')
|
||||||
|
.send()
|
||||||
|
.set('Cookie', [authToken])
|
||||||
|
.set('Accept', 'application/json')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.end((err, res) => {
|
||||||
|
if (err) done(err);
|
||||||
|
|
||||||
|
expect(res.body.torrents == null).toBe(false);
|
||||||
|
const torrentList: TorrentList = res.body.torrents;
|
||||||
|
|
||||||
|
expect(Object.keys(torrentList).length).toBeGreaterThanOrEqual(
|
||||||
|
torrentFiles.length + (torrentHash !== '' ? 1 : 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('PATCH /api/torrents/trackers', () => {
|
describe('PATCH /api/torrents/trackers', () => {
|
||||||
const testTrackers = [
|
const testTrackers = [
|
||||||
`https://${crypto.randomBytes(8).toString('hex')}.com/announce`,
|
`https://${crypto.randomBytes(8).toString('hex')}.com/announce`,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import path from 'path';
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import geoip from 'geoip-country';
|
import geoip from 'geoip-country';
|
||||||
import {moveSync} from 'fs-extra';
|
import {moveSync} from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
|
|
||||||
import type {ClientSettings} from '@shared/types/ClientSettings';
|
import type {ClientSettings} from '@shared/types/ClientSettings';
|
||||||
@@ -31,6 +32,7 @@ import ClientGatewayService from '../interfaces/clientGatewayService';
|
|||||||
import ClientRequestManager from './clientRequestManager';
|
import ClientRequestManager from './clientRequestManager';
|
||||||
import scgiUtil from './util/scgiUtil';
|
import scgiUtil from './util/scgiUtil';
|
||||||
import {getMethodCalls, processMethodCallResponse} from './util/rTorrentMethodCallUtil';
|
import {getMethodCalls, processMethodCallResponse} from './util/rTorrentMethodCallUtil';
|
||||||
|
import {getTempPath} from '../../models/TemporaryStorage';
|
||||||
import torrentFileUtil from '../../util/torrentFileUtil';
|
import torrentFileUtil from '../../util/torrentFileUtil';
|
||||||
import {
|
import {
|
||||||
encodeTags,
|
encodeTags,
|
||||||
@@ -55,41 +57,25 @@ class RTorrentClientGatewayService extends ClientGatewayService {
|
|||||||
clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings);
|
clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings);
|
||||||
|
|
||||||
async addTorrentsByFile({files, destination, tags, isBasePath, start}: AddTorrentByFileOptions): Promise<void> {
|
async addTorrentsByFile({files, destination, tags, isBasePath, start}: AddTorrentByFileOptions): Promise<void> {
|
||||||
const destinationPath = sanitizePath(destination);
|
const tempPath = path.join(
|
||||||
|
getTempPath(this.user._id),
|
||||||
|
'torrents',
|
||||||
|
`${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
|
||||||
|
);
|
||||||
|
await createDirectory(tempPath);
|
||||||
|
|
||||||
if (!isAllowedPath(destinationPath)) {
|
const torrentPaths = await Promise.all(
|
||||||
throw accessDeniedError();
|
files.map(async (file, index) => {
|
||||||
}
|
const torrentPath = path.join(tempPath, `${index}.torrent`);
|
||||||
|
fs.writeFileSync(torrentPath, Buffer.from(file, 'base64'), {});
|
||||||
await createDirectory(destinationPath);
|
return torrentPath;
|
||||||
|
|
||||||
// Each torrent is sent individually because rTorrent might have small
|
|
||||||
// XMLRPC request size limit. This allows the user to send files reliably.
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (file) => {
|
|
||||||
const additionalCalls: Array<string> = [];
|
|
||||||
|
|
||||||
additionalCalls.push(`${isBasePath ? 'd.directory_base.set' : 'd.directory.set'}="${destinationPath}"`);
|
|
||||||
|
|
||||||
if (Array.isArray(tags)) {
|
|
||||||
additionalCalls.push(`d.custom1.set=${encodeTags(tags)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
additionalCalls.push(`d.custom.set=addtime,${Date.now() / 1000}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
this.clientRequestManager
|
|
||||||
.methodCall(
|
|
||||||
start ? 'load.raw_start' : 'load.raw',
|
|
||||||
['', Buffer.from(file, 'base64')].concat(additionalCalls),
|
|
||||||
)
|
|
||||||
.then(this.processClientRequestSuccess, this.processClientRequestError)
|
|
||||||
.then(() => {
|
|
||||||
// returns nothing.
|
|
||||||
}) || Promise.reject()
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Delete temp files after 5 minutes. This is more than enough.
|
||||||
|
setTimeout(() => fs.rmdirSync(tempPath, {recursive: true}), 1000 * 60 * 5);
|
||||||
|
|
||||||
|
return this.addTorrentsByURL({urls: torrentPaths, destination, tags, isBasePath, start});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTorrentsByURL({urls, destination, tags, isBasePath, start}: AddTorrentByURLOptions): Promise<void> {
|
async addTorrentsByURL({urls, destination, tags, isBasePath, start}: AddTorrentByURLOptions): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user