Skip to content

Commit

Permalink
Fix rTorrent injection incorrect paths (cross-seed#503)
Browse files Browse the repository at this point in the history
Closes cross-seed#502 .
Tested on:
1. Single-file unrenamed torrent
2. Multi-file unrenamed folder
3. Single-file renamed (without its name in path) torrent
4. Multi-file renamed (without its name in path) torrent

Also checks the data for torrent resume correctly.

---------

Co-authored-by: Michael Goodnow <[email protected]>
  • Loading branch information
uraid and mmgoodnow authored Nov 2, 2023
1 parent 0834729 commit 9c802e9
Showing 1 changed file with 62 additions and 38 deletions.
100 changes: 62 additions & 38 deletions src/clients/RTorrent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { promises as fs, Stats } from "fs";
import { dirname, resolve } from "path";
import { dirname, join, resolve, sep } from "path";
import { inspect } from "util";
import xmlrpc, { Client } from "xmlrpc";
import { InjectionResult } from "../constants.js";
Expand All @@ -22,21 +22,37 @@ interface LibTorrentResume {
bitfield: number;
files: LibTorrentResumeFileEntry[];
}

interface DownloadLocation {
/**
* directoryBase is the root directory of a multi-file torrent,
* or the parent directory of a single-file torrent.
*/
directoryBase: string;
basePath: string;
downloadDir: string;
}

async function createLibTorrentResumeTree(
meta: Metafile,
dataDir: string
basePath: string
): Promise<LibTorrentResume> {
async function getFileResumeData(
file: File
): Promise<LibTorrentResumeFileEntry> {
const filePath = resolve(dataDir, file.path);
const filePathWithoutFirstSegment = file.path
.split(sep)
.slice(1)
.join(sep);

const resolvedFilePath = resolve(basePath, filePathWithoutFirstSegment);
const fileStat = await fs
.lstat(filePath)
.stat(resolvedFilePath)
.catch(() => ({ isFile: () => false } as Stats));
if (!fileStat.isFile() || fileStat.size !== file.length) {
logger.debug({
label: Label.RTORRENT,
message: `File ${filePath} either doesn't exist or is the wrong size.`,
message: `File ${resolvedFilePath} either doesn't exist or is the wrong size.`,
});
return {
completed: 0,
Expand All @@ -63,11 +79,11 @@ async function createLibTorrentResumeTree(
async function saveWithLibTorrentResume(
meta: Metafile,
savePath: string,
dataDir: string
basePath: string
): Promise<void> {
const rawWithLibtorrentResume = {
...meta.raw,
libtorrent_resume: await createLibTorrentResumeTree(meta, dataDir),
libtorrent_resume: await createLibTorrentResumeTree(meta, basePath),
};
await fs.writeFile(
savePath,
Expand Down Expand Up @@ -125,24 +141,20 @@ export default class RTorrent implements TorrentClient {
return downloadList.includes(infoHash.toUpperCase());
}

async checkOriginalTorrent(
private async checkOriginalTorrent(
searchee: Searchee
): Promise<
Result<
{ downloadDir: string },
{ directoryBase: string },
InjectionResult.FAILURE | InjectionResult.TORRENT_NOT_COMPLETE
>
> {
const infoHash = searchee.infoHash.toUpperCase();
type returnType = [["0" | "1"], [string], ["0" | "1"]];
type ReturnType = [[string], ["0" | "1"]];
let result;
try {
result = await this.methodCallP<returnType>("system.multicall", [
result = await this.methodCallP<ReturnType>("system.multicall", [
[
{
methodName: "d.is_multi_file",
params: [infoHash],
},
{
methodName: "d.directory",
params: [infoHash],
Expand All @@ -160,14 +172,12 @@ export default class RTorrent implements TorrentClient {

// temp diag for #154
try {
const [[isMultiFileStr], [dir], [isCompleteStr]] = result;
const [[directoryBase], [isCompleteStr]] = result;
const isComplete = Boolean(Number(isCompleteStr));
if (!isComplete) {
return resultOfErr(InjectionResult.TORRENT_NOT_COMPLETE);
}
return resultOf({
downloadDir: Number(isMultiFileStr) ? dirname(dir) : dir,
});
return resultOf({ directoryBase });
} catch (e) {
logger.error(e);
logger.debug("Failure caused by server response below:");
Expand All @@ -176,6 +186,34 @@ export default class RTorrent implements TorrentClient {
}
}

private async getDownloadLocation(
meta: Metafile,
searchee: Searchee,
path?: string
): Promise<
Result<
DownloadLocation,
InjectionResult.FAILURE | InjectionResult.TORRENT_NOT_COMPLETE
>
> {
if (path) {
const basePath = join(path, searchee.name);
const directoryBase = meta.isSingleFileTorrent ? path : basePath;
return resultOf({ downloadDir: path, basePath, directoryBase });
} else {
const result = await this.checkOriginalTorrent(searchee);
return result.mapOk(({ directoryBase }) => ({
directoryBase,
downloadDir: meta.isSingleFileTorrent
? directoryBase
: dirname(directoryBase),
basePath: meta.isSingleFileTorrent
? join(directoryBase, searchee.name)
: directoryBase,
}));
}
}

async validateConfig(): Promise<void> {
const { rtorrentRpcUrl } = getRuntimeConfig();
// no validation to do
Expand All @@ -202,37 +240,23 @@ export default class RTorrent implements TorrentClient {
return InjectionResult.ALREADY_EXISTS;
}

let downloadDir: string;

if (path) {
downloadDir = path;
} else {
const result = await this.checkOriginalTorrent(searchee);

if (result.isErr()) {
return result.unwrapErrOrThrow();
}

downloadDir = result.unwrapOrThrow().downloadDir;
}
const result = await this.getDownloadLocation(meta, searchee, path);
if (result.isErr()) return result.unwrapErrOrThrow();
const { directoryBase, basePath } = result.unwrapOrThrow();

const torrentFilePath = resolve(
outputDir,
`${meta.name}.tmp.${Date.now()}.torrent`
);

await saveWithLibTorrentResume(
meta,
torrentFilePath,
path ? path : downloadDir
);
await saveWithLibTorrentResume(meta, torrentFilePath, basePath);

for (let i = 0; i < 5; i++) {
try {
await this.methodCallP<void>("load.start", [
"",
torrentFilePath,
`d.directory.set="${downloadDir}"`,
`d.directory_base.set="${directoryBase}"`,
`d.custom1.set="cross-seed"`,
`d.custom.set=addtime,${Math.round(Date.now() / 1000)}`,
]);
Expand Down

0 comments on commit 9c802e9

Please sign in to comment.