mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-09 18:59:36 +00:00
feat(torrents): harden Aria2 torrent downloads
This commit is contained in:
@@ -84,6 +84,26 @@ describe('add download metadata workflow', () => {
|
||||
expect(isYouTubePlaylistUrl('https://example.com/playlist?list=PL123')).toBe(false);
|
||||
});
|
||||
|
||||
it('admits magnets and local torrent files through the Add window metadata path', () => {
|
||||
const rows = reconcileDownloadRows(
|
||||
'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=Example\nfile:///tmp/Example.torrent',
|
||||
[]
|
||||
);
|
||||
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]).toMatchObject({
|
||||
isTorrent: true,
|
||||
isMedia: false,
|
||||
status: 'loading'
|
||||
});
|
||||
expect(rows[1]).toMatchObject({
|
||||
isTorrent: true,
|
||||
isMedia: false,
|
||||
sourceUrl: 'file:///tmp/Example.torrent',
|
||||
status: 'loading'
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a playlist as one loading row until discovery succeeds', () => {
|
||||
const rows = reconcileDownloadRows(
|
||||
'https://www.youtube.com/playlist?list=PL123',
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isMediaUrl
|
||||
} from './downloads';
|
||||
import type { MediaPlaylistMetadata } from '../bindings/MediaPlaylistMetadata';
|
||||
import type { TorrentFile } from '../bindings/TorrentFile';
|
||||
import i18n from '../i18n';
|
||||
import { localePluralVariant } from '../i18n/locales';
|
||||
|
||||
@@ -52,6 +53,11 @@ export interface AddDownloadDraftRow {
|
||||
playlistError?: string;
|
||||
metadataBlockedReason?: 'unsafe-url';
|
||||
selected?: boolean;
|
||||
isTorrent?: boolean;
|
||||
torrentPath?: string;
|
||||
torrentInfoHash?: string;
|
||||
torrentFiles?: TorrentFile[];
|
||||
selectedTorrentFileIndices?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,12 +67,27 @@ export interface AddDownloadDraftRow {
|
||||
*/
|
||||
export const durableDownloadUrl = (sourceUrl: string): string => sourceUrl.trim();
|
||||
|
||||
const ALLOWED_SCHEMES = new Set(['http:', 'https:', 'ftp:', 'sftp:']);
|
||||
const ALLOWED_SCHEMES = new Set(['http:', 'https:', 'ftp:', 'sftp:', 'magnet:']);
|
||||
|
||||
const isLocalTorrentPath = (value: string): boolean => {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.protocol === 'file:') {
|
||||
return parsed.pathname.toLowerCase().endsWith('.torrent');
|
||||
}
|
||||
} catch {
|
||||
// A native Windows path is not a URL, even though URL parsing may treat
|
||||
// its drive letter as a scheme.
|
||||
}
|
||||
return value.toLowerCase().endsWith('.torrent')
|
||||
&& (value.startsWith('/') || /^[a-z]:[\\/]/i.test(value));
|
||||
};
|
||||
|
||||
type ParsedInput = {
|
||||
identity: string;
|
||||
sourceUrl: string;
|
||||
valid: boolean;
|
||||
isTorrent?: boolean;
|
||||
isPlaylist?: boolean;
|
||||
playlistSourceUrl?: string;
|
||||
playlistTitle?: string;
|
||||
@@ -115,10 +136,18 @@ const parseInputLines = (
|
||||
|
||||
let sourceUrl = line;
|
||||
let valid = false;
|
||||
let isTorrent = false;
|
||||
if (isLocalTorrentPath(line)) {
|
||||
valid = true;
|
||||
isTorrent = true;
|
||||
}
|
||||
try {
|
||||
const url = new URL(line);
|
||||
valid = ALLOWED_SCHEMES.has(url.protocol);
|
||||
if (valid) sourceUrl = url.href;
|
||||
if (!isTorrent) {
|
||||
const url = new URL(line);
|
||||
valid = ALLOWED_SCHEMES.has(url.protocol);
|
||||
isTorrent = valid && url.protocol === 'magnet:';
|
||||
if (valid) sourceUrl = url.href;
|
||||
}
|
||||
} catch {
|
||||
valid = false;
|
||||
}
|
||||
@@ -166,6 +195,7 @@ const parseInputLines = (
|
||||
identity,
|
||||
sourceUrl,
|
||||
valid,
|
||||
isTorrent,
|
||||
isPlaylist: valid && isYouTubePlaylistUrl(sourceUrl),
|
||||
requestContextVersion: valid ? requestContextVersions[sourceUrl] : undefined,
|
||||
selected: selectedBySourceUrl[sourceUrl] !== false
|
||||
@@ -219,6 +249,7 @@ export const reconcileDownloadRows = (
|
||||
generation: preserved.generation + 1,
|
||||
requestContextVersion,
|
||||
isMedia: preserved.isMedia || forcedMedia || Boolean(input.playlistSourceUrl),
|
||||
isTorrent: input.isTorrent,
|
||||
size: undefined,
|
||||
sizeBytes: undefined,
|
||||
resumable: undefined,
|
||||
@@ -235,7 +266,11 @@ export const reconcileDownloadRows = (
|
||||
playlistCount: input.playlistCount,
|
||||
playlistEntryTitle: input.playlistEntryTitle,
|
||||
playlistError: undefined,
|
||||
metadataBlockedReason: undefined
|
||||
metadataBlockedReason: undefined,
|
||||
torrentPath: input.isTorrent ? preserved.torrentPath : undefined,
|
||||
torrentInfoHash: input.isTorrent ? preserved.torrentInfoHash : undefined,
|
||||
torrentFiles: input.isTorrent ? preserved.torrentFiles : undefined,
|
||||
selectedTorrentFileIndices: input.isTorrent ? preserved.selectedTorrentFileIndices : undefined
|
||||
};
|
||||
}
|
||||
return preserved;
|
||||
@@ -263,6 +298,7 @@ export const reconcileDownloadRows = (
|
||||
|| forceMediaUrls.has(input.sourceUrl)
|
||||
|| isMediaUrl(input.sourceUrl)
|
||||
),
|
||||
isTorrent: input.valid && Boolean(input.isTorrent),
|
||||
isPlaylist: input.isPlaylist,
|
||||
playlistSourceUrl: input.playlistSourceUrl,
|
||||
playlistTitle: input.playlistTitle,
|
||||
|
||||
@@ -68,6 +68,7 @@ describe('download connection resolution', () => {
|
||||
describe('download filename matching', () => {
|
||||
it('matches frontend filenames to the backend Windows device-name canonicalization', () => {
|
||||
expect(canonicalizeDownloadFileName('CON.txt')).toBe('CON-.txt');
|
||||
expect(canonicalizeDownloadFileName('CON .txt')).toBe('CON -.txt');
|
||||
expect(canonicalizeDownloadFileName('com1.archive.zip')).toBe('com1.archive-.zip');
|
||||
expect(downloadFileNamesMatch('CON-.txt', 'CON.txt')).toBe(true);
|
||||
expect(downloadFileNamesMatch('console.txt', 'CON.txt')).toBe(false);
|
||||
|
||||
@@ -162,7 +162,7 @@ export const canonicalizeDownloadFileName = (fileName: string): string => {
|
||||
.trim()
|
||||
.replace(/[. ]+$/g, '');
|
||||
let canonical = sanitized && sanitized !== '.' && sanitized !== '..' ? sanitized : 'download';
|
||||
const reservedStem = canonical.split('.')[0]?.toUpperCase();
|
||||
const reservedStem = canonical.split('.')[0]?.trimEnd().toUpperCase();
|
||||
if (reservedStem && WINDOWS_RESERVED_FILENAME_STEMS.has(reservedStem)) {
|
||||
const extensionStart = canonical.lastIndexOf('.');
|
||||
const base = extensionStart > 0 ? canonical.slice(0, extensionStart) : canonical;
|
||||
|
||||
Reference in New Issue
Block a user