From b0add0db993ff6018e84f9471150b8c24a5ce023 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Tue, 20 Oct 2020 21:28:48 +0800 Subject: [PATCH] server: fetch torrents from http/https URLs directly from Flood This allows more control over the fetching process. Specifically, it allows us to add a feature to attach Cookies to requests in a later commit. --- server/.jest/auth.config.js | 1 + server/.jest/test.config.js | 1 + .../services/rTorrent/clientGatewayService.ts | 40 +++++++------ server/util/tempFileUtil.ts | 56 +++++++++++++++++++ 4 files changed, 80 insertions(+), 18 deletions(-) create mode 100644 server/util/tempFileUtil.ts diff --git a/server/.jest/auth.config.js b/server/.jest/auth.config.js index 07694693..e41e18b2 100644 --- a/server/.jest/auth.config.js +++ b/server/.jest/auth.config.js @@ -1,6 +1,7 @@ module.exports = { preset: 'ts-jest/presets/js-with-babel', rootDir: './../', + testEnvironment: 'node', testMatch: ['/routes/api/auth.test.ts'], setupFilesAfterEnv: ['/.jest/auth.setup.js'], globals: { diff --git a/server/.jest/test.config.js b/server/.jest/test.config.js index b74f4a60..07469dd4 100644 --- a/server/.jest/test.config.js +++ b/server/.jest/test.config.js @@ -1,6 +1,7 @@ module.exports = { preset: 'ts-jest/presets/js-with-babel', rootDir: './../', + testEnvironment: 'node', testPathIgnorePatterns: ['auth.test.ts'], setupFilesAfterEnv: ['/.jest/test.setup.js'], globals: { diff --git a/server/services/rTorrent/clientGatewayService.ts b/server/services/rTorrent/clientGatewayService.ts index 6a706caa..391e90d1 100644 --- a/server/services/rTorrent/clientGatewayService.ts +++ b/server/services/rTorrent/clientGatewayService.ts @@ -1,4 +1,3 @@ -import crypto from 'crypto'; import fs from 'fs'; import geoip from 'geoip-country'; import {moveSync} from 'fs-extra'; @@ -32,7 +31,7 @@ import ClientGatewayService from '../interfaces/clientGatewayService'; import ClientRequestManager from './clientRequestManager'; import scgiUtil from './util/scgiUtil'; import {getMethodCalls, processMethodCallResponse} from './util/rTorrentMethodCallUtil'; -import {getTempPath} from '../../models/TemporaryStorage'; +import {fetchURLToTempFile, saveBufferToTempFile} from '../../util/tempFileUtil'; import torrentFileUtil from '../../util/torrentFileUtil'; import { encodeTags, @@ -57,24 +56,12 @@ class RTorrentClientGatewayService extends ClientGatewayService { clientRequestManager = new ClientRequestManager(this.user.client as RTorrentConnectionSettings); async addTorrentsByFile({files, destination, tags, isBasePath, start}: AddTorrentByFileOptions): Promise { - const tempPath = path.join( - getTempPath(this.user._id), - 'torrents', - `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`, - ); - await createDirectory(tempPath); - const torrentPaths = await Promise.all( - files.map(async (file, index) => { - const torrentPath = path.join(tempPath, `${index}.torrent`); - fs.writeFileSync(torrentPath, Buffer.from(file, 'base64'), {}); - return torrentPath; + files.map(async (file) => { + return saveBufferToTempFile(Buffer.from(file, 'base64'), 'torrent'); }), ); - // 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}); } @@ -87,7 +74,24 @@ class RTorrentClientGatewayService extends ClientGatewayService { await createDirectory(destinationPath); - const methodCalls: MultiMethodCalls = urls.map((url) => { + const torrentPaths = await Promise.all( + urls.map(async (url) => { + if (/^(http|https):\/\//.test(url)) { + // TODO: properly handle error and let frontend know + const torrentPath = await fetchURLToTempFile(url, 'torrent').catch((e) => console.error(e)); + + if (typeof torrentPath === 'string') { + return torrentPath; + } + } + + // TODO: handle potential other types of downloads + + return url; + }), + ); + + const methodCalls: MultiMethodCalls = torrentPaths.map((torrentPath) => { const additionalCalls: Array = []; additionalCalls.push(`${isBasePath ? 'd.directory_base.set' : 'd.directory.set'}="${destinationPath}"`); @@ -100,7 +104,7 @@ class RTorrentClientGatewayService extends ClientGatewayService { return { methodName: start ? 'load.start' : 'load.normal', - params: ['', url].concat(additionalCalls), + params: ['', torrentPath].concat(additionalCalls), }; }, []); diff --git a/server/util/tempFileUtil.ts b/server/util/tempFileUtil.ts new file mode 100644 index 00000000..60c58070 --- /dev/null +++ b/server/util/tempFileUtil.ts @@ -0,0 +1,56 @@ +import axios, {AxiosResponse} from 'axios'; +import crypto from 'crypto'; +import fs from 'fs'; + +import {getTempPath} from '../models/TemporaryStorage'; + +/** + * Gets a randomly generated file path for temp file. + * + * @return {string} - path + */ +const getTempFilePath = (extension = 'tmp'): string => { + return getTempPath(`${Date.now()}-${crypto.randomBytes(8).toString('hex')}.${extension}`); +}; + +/** + * Saves buffer to temporary storage as a file. + * + * @param {Buffer} buffer - buffer + * @param {string} extension - file extension of temp file + * @return {string} - path of saved temporary file. deleted after 5 minutes. + */ +export const saveBufferToTempFile = async (buffer: Buffer, extension?: string): Promise => { + const tempPath = getTempFilePath(extension); + + fs.writeFileSync(tempPath, buffer); + + setTimeout(() => fs.unlinkSync(tempPath), 1000 * 60 * 5); + + return tempPath; +}; + +/** + * Fetches from URL to temporary storage. + * + * @param {string} url - URL + * @param {string} extension - file extension of temp file + * @return {string} - path of saved temporary file. deleted after 5 minutes. + */ +export const fetchURLToTempFile = async (url: string, extension?: string): Promise => { + const tempPath = getTempFilePath(extension); + + await new Promise((resolve) => { + axios({ + method: 'GET', + url, + responseType: 'stream', + }).then((res: AxiosResponse) => { + res.data.pipe(fs.createWriteStream(tempPath)).on('finish', () => resolve()); + }); + }); + + setTimeout(() => fs.unlinkSync(tempPath), 1000 * 60 * 5); + + return tempPath; +};