API: torrents: return hashes of added torrents when possible

This commit is contained in:
Jesse Chan
2021-01-31 15:06:23 +00:00
parent 38ee8da3ba
commit 12f08a9313
8 changed files with 647 additions and 229 deletions
+501 -132
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -101,6 +101,7 @@
"@types/nedb": "^1.8.11",
"@types/node": "^12.19.16",
"@types/overlayscrollbars": "^1.12.0",
"@types/parse-torrent": "^5.8.3",
"@types/passport": "^1.0.6",
"@types/passport-jwt": "^3.0.4",
"@types/react": "^17.0.1",
@@ -175,6 +176,7 @@
"nedb-promises": "^4.1.1",
"overlayscrollbars": "^1.13.1",
"overlayscrollbars-react": "^0.2.2",
"parse-torrent": "^9.1.3",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"postcss": "^8.2.6",
+2 -2
View File
@@ -101,7 +101,7 @@ describe('POST /api/torrents/add-urls', () => {
})
.set('Cookie', [authToken])
.set('Accept', 'application/json')
.expect(500)
.expect(403)
.expect('Content-Type', /json/)
.end((err, res) => {
if (err) done(err);
@@ -208,7 +208,7 @@ describe('POST /api/torrents/add-files', () => {
})
.set('Cookie', [authToken])
.set('Accept', 'application/json')
.expect(500)
.expect(403)
.expect('Content-Type', /json/)
.end((err, res) => {
if (err) done(err);
+42 -24
View File
@@ -111,12 +111,13 @@ router.get('/', (req, res) => {
* @tags Torrents
* @security User
* @param {AddTorrentByURLOptions} request.body.required - options - application/json
* @return {object} 200 - success response - application/json
* @return {object} 200 - all torrents added - application/json
* @return {object} 202 - requests sent to torrent client - application/json
* @return {object} 207 - some succeed, some failed - application/json
* @return {Error} 403 - illegal destination - application/json
* @return {Error} 500 - failure response - application/json
*/
router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, res) => {
const callback = getResponseFn(res);
const parsedResult = addTorrentByURLSchema.safeParse(req.body);
if (!parsedResult.success) {
@@ -142,7 +143,8 @@ router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, r
});
if (finalDestination == null) {
callback(null, accessDeniedError());
const {code, message} = accessDeniedError();
res.status(403).json({code, message});
return;
}
@@ -158,14 +160,21 @@ router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, r
isInitialSeeding: isInitialSeeding ?? false,
start: start ?? false,
})
.then((response) => {
req.services?.torrentService.fetchTorrentList();
return response;
})
.then(callback)
.catch((err) => {
callback(null, err);
});
.then(
(response) => {
req.services?.torrentService.fetchTorrentList();
if (response.length === 0) {
res.status(202).json(response);
} else if (response.length < urls.length) {
res.status(207).json(response);
} else {
res.status(200).json(response);
}
},
({code, message}) => {
res.status(500).json({code, message});
},
);
});
/**
@@ -174,12 +183,13 @@ router.post<unknown, unknown, AddTorrentByURLOptions>('/add-urls', async (req, r
* @tags Torrents
* @security User
* @param {AddTorrentByFileOptions} request.body.required - options - application/json
* @return {object} 200 - success response - application/json
* @return {object} 200 - all torrents added - application/json
* @return {object} 202 - requests sent to torrent client - application/json
* @return {object} 207 - some succeed, some failed - application/json
* @return {Error} 403 - illegal destination - application/json
* @return {Error} 500 - failure response - application/json
*/
router.post<unknown, unknown, AddTorrentByFileOptions>('/add-files', async (req, res) => {
const callback = getResponseFn(res);
const parsedResult = addTorrentByFileSchema.safeParse(req.body);
if (!parsedResult.success) {
@@ -195,7 +205,8 @@ router.post<unknown, unknown, AddTorrentByFileOptions>('/add-files', async (req,
});
if (finalDestination == null) {
callback(null, accessDeniedError());
const {code, message} = accessDeniedError();
res.status(403).json({code, message});
return;
}
@@ -210,14 +221,21 @@ router.post<unknown, unknown, AddTorrentByFileOptions>('/add-files', async (req,
isInitialSeeding: isInitialSeeding ?? false,
start: start ?? false,
})
.then((response) => {
req.services?.torrentService.fetchTorrentList();
return response;
})
.then(callback)
.catch((err) => {
callback(null, err);
});
.then(
(response) => {
req.services?.torrentService.fetchTorrentList();
if (response.length === 0) {
res.status(202).json(response);
} else if (response.length < files.length) {
res.status(207).json(response);
} else {
res.status(200).json(response);
}
},
({code, message}) => {
res.status(500).json({code, message});
},
);
});
/**
@@ -43,8 +43,8 @@ class TransmissionClientGatewayService extends ClientGatewayService {
tags,
isCompleted,
start,
}: Required<AddTorrentByFileOptions>): Promise<void> {
const addedTorrents: [string, ...string[]] = await Promise.all(
}: Required<AddTorrentByFileOptions>): Promise<string[]> {
const addedTorrents = await Promise.all(
files.map(async (file) => {
const {hashString} =
(await this.clientRequestManager
@@ -57,23 +57,22 @@ class TransmissionClientGatewayService extends ClientGatewayService {
.catch(() => undefined)) || {};
return hashString;
}),
)
.then((results) => results.filter((hash) => hash != null) as string[])
.then((hashes) => {
if (hashes.length < 1) {
throw new Error();
}
return hashes as [string, ...string[]];
});
).then((results) => results.filter((hash) => hash) as string[]);
if (addedTorrents[0] == null) {
throw new Error();
}
if (tags.length > 0) {
await this.setTorrentsTags({hashes: addedTorrents, tags});
await this.setTorrentsTags({hashes: addedTorrents as [string, ...string[]], tags});
}
if (isCompleted) {
// Transmission doesn't support skipping verification
this.checkTorrents({hashes: addedTorrents}).catch(() => undefined);
}
return addedTorrents;
}
async addTorrentsByURL({
@@ -81,10 +80,9 @@ class TransmissionClientGatewayService extends ClientGatewayService {
cookies,
destination,
tags,
isCompleted,
start,
}: Required<AddTorrentByURLOptions>): Promise<void> {
const addedTorrents: [string, ...string[]] = await Promise.all(
}: Required<AddTorrentByURLOptions>): Promise<string[]> {
const addedTorrents = await Promise.all(
urls.map(async (url) => {
const domain = url.split('/')[2];
const {hashString} =
@@ -99,23 +97,17 @@ class TransmissionClientGatewayService extends ClientGatewayService {
.catch(() => undefined)) || {};
return hashString;
}),
)
.then((results) => results.filter((hash) => hash != null) as string[])
.then((hashes) => {
if (hashes.length < 1) {
throw new Error();
}
return hashes as [string, ...string[]];
});
).then((results) => results.filter((hash) => hash) as string[]);
if (addedTorrents[0] == null) {
throw new Error();
}
if (tags.length > 0) {
await this.setTorrentsTags({hashes: addedTorrents, tags});
await this.setTorrentsTags({hashes: addedTorrents as [string, ...string[]], tags});
}
if (isCompleted) {
// Transmission doesn't support skipping verification
this.checkTorrents({hashes: addedTorrents}).catch(() => undefined);
}
return addedTorrents;
}
async checkTorrents({hashes}: CheckTorrentsOptions): Promise<void> {
@@ -41,17 +41,17 @@ abstract class ClientGatewayService extends BaseService<ClientGatewayServiceEven
* Adds torrents by file
*
* @param {Required<AddTorrentByFileOptions>} options - An object of options...
* @return {Promise<void>} - Rejects with error.
* @return {Promise<string[]>} - Resolves with an array of hashes of added torrents or rejects with error.
*/
abstract addTorrentsByFile(options: Required<AddTorrentByFileOptions>): Promise<void>;
abstract addTorrentsByFile(options: Required<AddTorrentByFileOptions>): Promise<string[]>;
/**
* Adds torrents by URL
*
* @param {Required<AddTorrentByURLOptions>} options - An object of options...
* @return {Promise<void>} - Rejects with error.
* @return {Promise<string[]>} - Resolves with an array of hashes of added torrents or rejects with error.
*/
abstract addTorrentsByURL(options: Required<AddTorrentByURLOptions>): Promise<void>;
abstract addTorrentsByURL(options: Required<AddTorrentByURLOptions>): Promise<string[]>;
/**
* Checks torrents
@@ -1,4 +1,5 @@
import {homedir} from 'os';
import parseTorrent from 'parse-torrent';
import path from 'path';
import type {
@@ -50,16 +51,34 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
tags,
isBasePath,
isCompleted,
isInitialSeeding,
isSequential,
start,
}: Required<AddTorrentByFileOptions>): Promise<void> {
// TODO: isInitialSeeding not implemented
}: Required<AddTorrentByFileOptions>): Promise<string[]> {
const fileBuffers: Buffer[] = [];
const fileBuffers = files.map((file) => {
return Buffer.from(file, 'base64');
});
const torrentHashes: string[] = (
await Promise.all(
files.map(async (file) => {
try {
const fileBuffer = Buffer.from(file, 'base64');
return this.clientRequestManager
const {infoHash} = parseTorrent(fileBuffer);
fileBuffers.push(fileBuffer);
return infoHash;
} catch {
return;
}
}),
)
).filter((hash) => hash) as string[];
if (torrentHashes[0] == null) {
throw new Error();
}
await this.clientRequestManager
.torrentsAddFiles(fileBuffers, {
savepath: destination,
tags: tags.join(','),
@@ -70,6 +89,10 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
skip_checking: isCompleted,
})
.then(this.processClientRequestSuccess, this.processClientRequestError);
await this.setTorrentsInitialSeeding({hashes: torrentHashes, isInitialSeeding});
return torrentHashes;
}
async addTorrentsByURL({
@@ -81,10 +104,10 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
isCompleted,
isSequential,
start,
}: Required<AddTorrentByURLOptions>): Promise<void> {
}: Required<AddTorrentByURLOptions>): Promise<string[]> {
// TODO: isInitialSeeding not implemented
return this.clientRequestManager
await this.clientRequestManager
.torrentsAddURLs(urls, {
cookie: cookies != null ? Object.values(cookies)[0]?.[0] : undefined,
savepath: destination,
@@ -96,6 +119,8 @@ class QBittorrentClientGatewayService extends ClientGatewayService {
skip_checking: isCompleted,
})
.then(this.processClientRequestSuccess, this.processClientRequestError);
return [];
}
async checkTorrents({hashes}: CheckTorrentsOptions): Promise<void> {
@@ -2,6 +2,7 @@ import fs from 'fs';
import geoip from 'geoip-country';
import {moveSync} from 'fs-extra';
import path from 'path';
import parseTorrent from 'parse-torrent';
import sanitize from 'sanitize-filename';
import type {
@@ -66,7 +67,7 @@ class RTorrentClientGatewayService extends ClientGatewayService {
isSequential,
isInitialSeeding,
start,
}: Required<AddTorrentByFileOptions>): Promise<void> {
}: Required<AddTorrentByFileOptions>): Promise<string[]> {
const torrentPaths = await Promise.all(
files.map(async (file) => {
return saveBufferToTempFile(Buffer.from(file, 'base64'), 'torrent', {
@@ -75,21 +76,17 @@ class RTorrentClientGatewayService extends ClientGatewayService {
}),
);
if (torrentPaths[0] != null) {
return this.addTorrentsByURL({
urls: torrentPaths as [string, ...string[]],
cookies: {},
destination,
tags,
isBasePath,
isCompleted,
isSequential,
isInitialSeeding,
start,
});
}
return Promise.reject();
return this.addTorrentsByURL({
urls: torrentPaths as [string, ...string[]],
cookies: {},
destination,
tags,
isBasePath,
isCompleted,
isSequential,
isInitialSeeding,
start,
});
}
async addTorrentsByURL({
@@ -102,7 +99,7 @@ class RTorrentClientGatewayService extends ClientGatewayService {
isSequential,
isInitialSeeding,
start,
}: Required<AddTorrentByURLOptions>): Promise<void> {
}: Required<AddTorrentByURLOptions>): Promise<string[]> {
await fs.promises.mkdir(destination, {recursive: true});
const torrentPaths: Array<string> = (
@@ -123,23 +120,39 @@ class RTorrentClientGatewayService extends ClientGatewayService {
return '';
}
// TODO: handle potential other types of downloads
return url;
}),
)
).filter((torrentPath) => torrentPath !== '');
if (isCompleted) {
const torrentHashes: string[] = (
await Promise.all(
torrentPaths.map((torrentPath) => {
if (!fs.existsSync(torrentPath)) {
return false;
}
torrentPaths.map(
async (torrentPath): Promise<string | undefined> => {
try {
if (torrentPath.startsWith('magnet:')) {
return parseTorrent(torrentPath).infoHash;
}
return setCompleted(torrentPath, destination, isBasePath);
}),
);
if (!fs.existsSync(torrentPath)) {
return;
}
if (isCompleted) {
await setCompleted(torrentPath, destination, isBasePath);
}
return parseTorrent(fs.readFileSync(torrentPath)).infoHash;
} catch {
return;
}
},
),
)
).filter((torrentHash) => torrentHash) as string[];
if (torrentHashes[0] == null) {
throw new Error();
}
const methodCalls: MultiMethodCalls = torrentPaths.map((torrentPath) => {
@@ -167,12 +180,11 @@ class RTorrentClientGatewayService extends ClientGatewayService {
};
}, []);
return this.clientRequestManager
await this.clientRequestManager
.methodCall('system.multicall', [methodCalls])
.then(this.processClientRequestSuccess, this.processClientRequestError)
.then(() => {
// returns nothing.
});
.then(this.processClientRequestSuccess, this.processClientRequestError);
return torrentHashes;
}
async checkTorrents({hashes}: CheckTorrentsOptions): Promise<void> {