mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-02 15:39:37 +00:00
feat(torrents): add integrity verification policy
This commit is contained in:
@@ -627,8 +627,9 @@ async function main() {
|
||||
const seedRoot = path.join(seedParent, 'firelink-torrent-runtime');
|
||||
const probeDir = path.join(tempRoot, 'probe');
|
||||
const finalDir = path.join(tempRoot, 'final');
|
||||
const integrityDir = path.join(tempRoot, 'integrity');
|
||||
const cancelDir = path.join(tempRoot, 'cancel');
|
||||
for (const directory of [seedRoot, probeDir, finalDir, cancelDir]) fs.mkdirSync(directory, { recursive: true });
|
||||
for (const directory of [seedRoot, probeDir, finalDir, integrityDir, cancelDir]) fs.mkdirSync(directory, { recursive: true });
|
||||
|
||||
const seederListenPort = await findAvailablePort();
|
||||
const clientListenPort = await findAvailablePort();
|
||||
@@ -730,6 +731,27 @@ async function main() {
|
||||
assert(finalStatus.files?.some(file => file.path === selectedPath || file.path.endsWith('/selected.bin')), 'terminal status omitted selected output');
|
||||
console.log('[OK] selected addTorrent output, pause/resume, and Aria2 file ownership passed');
|
||||
|
||||
const integrityPath = path.join(integrityDir, torrent.name, 'selected.bin');
|
||||
fs.mkdirSync(path.dirname(integrityPath), { recursive: true });
|
||||
fs.writeFileSync(integrityPath, Buffer.alloc(torrent.files[0].data.length, 0x00));
|
||||
const integrityGid = await rpc(client.rpcPort, client.secret, 'aria2.addTorrent', [
|
||||
savedTorrentBytes.toString('base64'),
|
||||
[],
|
||||
{
|
||||
dir: integrityDir,
|
||||
'select-file': '1',
|
||||
'index-out': indexOut,
|
||||
'check-integrity': 'true',
|
||||
'bt-hash-check-seed': 'false',
|
||||
'seed-time': '0',
|
||||
'auto-file-renaming': 'false',
|
||||
},
|
||||
]);
|
||||
const integrityStatus = await waitForTerminal(client, integrityGid, 30000);
|
||||
assert(integrityStatus.status === 'complete', 'integrity-check torrent did not complete');
|
||||
assert(fs.readFileSync(integrityPath).equals(torrent.files[0].data), 'integrity check did not replace corrupted torrent data');
|
||||
console.log('[OK] check-integrity detected and repaired corrupted Torrent data');
|
||||
|
||||
const cancelGid = await rpc(client.rpcPort, client.secret, 'aria2.addTorrent', [
|
||||
savedTorrentBytes.toString('base64'),
|
||||
[],
|
||||
|
||||
@@ -191,6 +191,9 @@ pub struct DownloadItem {
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_peer_speed_limit: Option<String>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_check_integrity: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -214,6 +214,7 @@ pub struct SpawnPayload {
|
||||
pub torrent_upload_limit: Option<String>,
|
||||
pub torrent_max_peers: Option<u32>,
|
||||
pub torrent_peer_speed_limit: Option<String>,
|
||||
pub torrent_check_integrity: bool,
|
||||
}
|
||||
|
||||
/// A sidecar spawner. In production this calls the real aria2/yt-dlp
|
||||
@@ -3163,6 +3164,22 @@ fn apply_aria2_torrent_options(
|
||||
serde_json::json!(normalized),
|
||||
);
|
||||
}
|
||||
if payload.torrent_check_integrity {
|
||||
options.insert(
|
||||
"check-integrity".to_string(),
|
||||
serde_json::json!("true"),
|
||||
);
|
||||
options.insert(
|
||||
"bt-hash-check-seed".to_string(),
|
||||
serde_json::json!(torrent_seeding_requested(payload).to_string()),
|
||||
);
|
||||
// Do not let a daemon-wide bt-seed-unverified setting bypass the
|
||||
// explicit per-download integrity request.
|
||||
options.insert(
|
||||
"bt-seed-unverified".to_string(),
|
||||
serde_json::json!("false"),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3752,6 +3769,9 @@ pub struct EnqueueItem {
|
||||
pub torrent_peer_speed_limit: Option<String>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_check_integrity: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub lifecycle_generation: Option<String>,
|
||||
}
|
||||
|
||||
@@ -3799,6 +3819,7 @@ impl EnqueueItem {
|
||||
torrent_upload_limit: self.torrent_upload_limit,
|
||||
torrent_max_peers: self.torrent_max_peers,
|
||||
torrent_peer_speed_limit: self.torrent_peer_speed_limit,
|
||||
torrent_check_integrity: self.torrent_check_integrity.unwrap_or(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3913,6 +3934,85 @@ mod tests {
|
||||
assert!(torrent_seeding_requested(&payload));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_integrity_check_does_not_enter_seeding_without_a_seed_policy() {
|
||||
let mut options = serde_json::Map::new();
|
||||
let payload = SpawnPayload {
|
||||
is_torrent: true,
|
||||
torrent_check_integrity: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||
|
||||
assert_eq!(options.get("check-integrity"), Some(&serde_json::json!("true")));
|
||||
assert_eq!(
|
||||
options.get("bt-hash-check-seed"),
|
||||
Some(&serde_json::json!("false"))
|
||||
);
|
||||
assert_eq!(
|
||||
options.get("bt-seed-unverified"),
|
||||
Some(&serde_json::json!("false"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_integrity_check_preserves_an_explicit_seeding_policy() {
|
||||
let mut options = serde_json::Map::new();
|
||||
let payload = SpawnPayload {
|
||||
is_torrent: true,
|
||||
torrent_check_integrity: true,
|
||||
torrent_seed_ratio: Some(1.0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||
|
||||
assert_eq!(options.get("check-integrity"), Some(&serde_json::json!("true")));
|
||||
assert_eq!(
|
||||
options.get("bt-hash-check-seed"),
|
||||
Some(&serde_json::json!("true"))
|
||||
);
|
||||
assert_eq!(
|
||||
options.get("bt-seed-unverified"),
|
||||
Some(&serde_json::json!("false"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_integrity_options_are_not_emitted_when_disabled_or_non_torrent() {
|
||||
for payload in [
|
||||
SpawnPayload::default(),
|
||||
SpawnPayload {
|
||||
is_torrent: true,
|
||||
..Default::default()
|
||||
},
|
||||
] {
|
||||
let mut options = serde_json::Map::new();
|
||||
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||
assert!(!options.contains_key("check-integrity"));
|
||||
assert!(!options.contains_key("bt-hash-check-seed"));
|
||||
assert!(!options.contains_key("bt-seed-unverified"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enqueue_item_deserializes_integrity_policy_from_frontend_payload() {
|
||||
let item: EnqueueItem = serde_json::from_value(serde_json::json!({
|
||||
"id": "torrent-integrity",
|
||||
"queue_id": "main",
|
||||
"url": "magnet:?xt=urn:btih:0123456789012345678901234567890123456789",
|
||||
"destination": "/tmp/downloads",
|
||||
"filename": "payload",
|
||||
"is_media": false,
|
||||
"is_torrent": true,
|
||||
"torrent_check_integrity": true
|
||||
}))
|
||||
.expect("frontend enqueue payload should deserialize");
|
||||
|
||||
assert!(item.into_task().payload.torrent_check_integrity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_options_reject_invalid_seed_values() {
|
||||
let mut options = serde_json::Map::new();
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import type { DownloadCategory } from "./DownloadCategory";
|
||||
import type { DownloadStatus } from "./DownloadStatus";
|
||||
|
||||
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, downloadedBytes?: number, totalBytes?: number, totalIsEstimate?: boolean, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, mediaQuality?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, lastTry?: string, isTorrent?: boolean, torrentPath?: string, torrentFileIndices?: Array<number>, torrentInfoHash?: string, torrentSeedTime?: number, torrentSeedRatio?: number, torrentUploadLimit?: string, torrentMaxPeers?: number, torrentPeerSpeedLimit?: string, };
|
||||
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, downloadedBytes?: number, totalBytes?: number, totalIsEstimate?: boolean, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, mediaQuality?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, lastTry?: string, isTorrent?: boolean, torrentPath?: string, torrentFileIndices?: Array<number>, torrentInfoHash?: string, torrentSeedTime?: number, torrentSeedRatio?: number, torrentUploadLimit?: string, torrentMaxPeers?: number, torrentPeerSpeedLimit?: string, torrentCheckIntegrity?: boolean, };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type EnqueueItem = { id: string, queue_id: string, url: string, destination: string, filename: string, connections: number | null, speed_limit: string | null, username: string | null, password: string | null, headers: string | null, checksum: string | null, cookies: string | null, mirrors: string | null, user_agent: string | null, max_tries: number | null, proxy: string | null, format_selector: string | null, cookie_source: string | null, is_media: boolean | null, is_torrent?: boolean, torrent_path?: string, torrent_file_indices?: Array<number>, torrent_info_hash?: string, torrent_seed_time?: number, torrent_seed_ratio?: number, torrent_upload_limit?: string, torrent_max_peers?: number, torrent_peer_speed_limit?: string, lifecycle_generation?: string, };
|
||||
export type EnqueueItem = { id: string, queue_id: string, url: string, destination: string, filename: string, connections: number | null, speed_limit: string | null, username: string | null, password: string | null, headers: string | null, checksum: string | null, cookies: string | null, mirrors: string | null, user_agent: string | null, max_tries: number | null, proxy: string | null, format_selector: string | null, cookie_source: string | null, is_media: boolean | null, is_torrent?: boolean, torrent_path?: string, torrent_file_indices?: Array<number>, torrent_info_hash?: string, torrent_seed_time?: number, torrent_seed_ratio?: number, torrent_upload_limit?: string, torrent_max_peers?: number, torrent_peer_speed_limit?: string, torrent_check_integrity?: boolean, lifecycle_generation?: string, };
|
||||
|
||||
@@ -232,6 +232,7 @@ export const AddDownloadsModal = () => {
|
||||
const [torrentUploadLimit, setTorrentUploadLimit] = useState('1024');
|
||||
const [torrentMaxPeers, setTorrentMaxPeers] = useState('');
|
||||
const [torrentPeerSpeedLimit, setTorrentPeerSpeedLimit] = useState('');
|
||||
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
||||
const [freeSpace, setFreeSpace] = useState('Unknown');
|
||||
const freeSpaceRequestRef = useRef(0);
|
||||
|
||||
@@ -372,6 +373,7 @@ export const AddDownloadsModal = () => {
|
||||
setTorrentUploadLimit('1024');
|
||||
setTorrentMaxPeers('');
|
||||
setTorrentPeerSpeedLimit('');
|
||||
setTorrentCheckIntegrity(false);
|
||||
setUseAuth(false);
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
@@ -1448,6 +1450,7 @@ export const AddDownloadsModal = () => {
|
||||
torrentPeerSpeedLimit: item.isTorrent
|
||||
? normalizeSpeedLimitForBackend(torrentPeerSpeedLimit) || undefined
|
||||
: undefined,
|
||||
torrentCheckIntegrity: item.isTorrent ? torrentCheckIntegrity : undefined,
|
||||
size: item.size || (item.sizeBytes ? formatBytes(item.sizeBytes) : undefined),
|
||||
sizeBytes: item.sizeBytes
|
||||
}, action);
|
||||
@@ -2094,6 +2097,20 @@ export const AddDownloadsModal = () => {
|
||||
<span className="text-text-muted">KiB/s</span>
|
||||
</div>
|
||||
) : null}
|
||||
<label className="flex items-start gap-2 text-text-primary pt-2 border-t border-border-modal/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={torrentCheckIntegrity}
|
||||
onChange={event => setTorrentCheckIntegrity(event.target.checked)}
|
||||
className="accent-blue-500 mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
<span className="block">{t($ => $.addDownloads.torrentVerifyIntegrity)}</span>
|
||||
<span className="block text-[10px] text-text-muted">
|
||||
{t($ => $.addDownloads.torrentVerifyIntegrityHint)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-[1fr_auto] gap-2 items-center pt-2 border-t border-border-modal/50">
|
||||
<label htmlFor="torrent-max-peers" className="text-text-muted">
|
||||
{t($ => $.addDownloads.torrentMaxPeers)}
|
||||
|
||||
@@ -77,6 +77,7 @@ export const PropertiesModal = () => {
|
||||
const [liveTorrentUploadLimitValue, setLiveTorrentUploadLimitValue] = useState('');
|
||||
const [liveTorrentMaxPeersValue, setLiveTorrentMaxPeersValue] = useState('');
|
||||
const [liveTorrentPeerSpeedLimitValue, setLiveTorrentPeerSpeedLimitValue] = useState('');
|
||||
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
||||
const [isLiveSpeedLimitPending, setIsLiveSpeedLimitPending] = useState(false);
|
||||
const [isLiveTorrentUploadLimitPending, setIsLiveTorrentUploadLimitPending] = useState(false);
|
||||
const [isLiveTorrentPeerOptionsPending, setIsLiveTorrentPeerOptionsPending] = useState(false);
|
||||
@@ -168,6 +169,7 @@ export const PropertiesModal = () => {
|
||||
activeItem.torrentMaxPeers === undefined ? '' : String(activeItem.torrentMaxPeers)
|
||||
);
|
||||
setLiveTorrentPeerSpeedLimitValue(activeItem.torrentPeerSpeedLimit || '');
|
||||
setTorrentCheckIntegrity(activeItem.torrentCheckIntegrity === true);
|
||||
setErrorMessage('');
|
||||
} else {
|
||||
setSelectedPropertiesDownloadId(null);
|
||||
@@ -279,6 +281,7 @@ export const PropertiesModal = () => {
|
||||
? {
|
||||
torrentMaxPeers: normalizedMaxPeers,
|
||||
torrentPeerSpeedLimit: normalizedPeerSpeedLimit || undefined,
|
||||
torrentCheckIntegrity,
|
||||
}
|
||||
: {}),
|
||||
...(connectionsDirty
|
||||
@@ -701,6 +704,23 @@ export const PropertiesModal = () => {
|
||||
<div className="col-start-2 text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentPeerOptionsSavedHint)}
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-check-integrity">
|
||||
{t($ => $.properties.torrentVerifyIntegrity)}
|
||||
</label>
|
||||
<label className="flex items-start gap-2 text-xs text-text-primary">
|
||||
<input
|
||||
id="torrent-check-integrity"
|
||||
type="checkbox"
|
||||
checked={torrentCheckIntegrity}
|
||||
onChange={event => setTorrentCheckIntegrity(event.currentTarget.checked)}
|
||||
disabled={transferLocked}
|
||||
className="accent-accent mt-0.5 disabled:opacity-50"
|
||||
aria-describedby="torrent-check-integrity-hint"
|
||||
/>
|
||||
<span id="torrent-check-integrity-hint" className="text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentVerifyIntegrityHint)}
|
||||
</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
{(liveSpeedLimitAvailable || liveSpeedLimitUnavailable) && (
|
||||
|
||||
@@ -236,6 +236,8 @@ const common = {
|
||||
liveTorrentPeerOptionsApply: 'Apply peer controls',
|
||||
liveTorrentPeerOptionsHint: 'Changes apply without replacing the active Torrent. Leave blank to use Aria2 defaults.',
|
||||
torrentPeerOptionsSavedHint: 'Saved per Torrent. 0 peers means unlimited; blank uses Aria2 defaults.',
|
||||
torrentVerifyIntegrity: 'Verify Torrent integrity',
|
||||
torrentVerifyIntegrityHint: 'Applied when this Torrent starts or retries. It may recheck pieces and download damaged data; active transfers cannot change it.',
|
||||
torrentMaxPeers: 'Maximum Torrent peers',
|
||||
torrentPeerSpeedLimit: 'Peer speed threshold',
|
||||
torrentMaxPeersInvalid: 'Torrent maximum peers must be an integer from 0 to 1000',
|
||||
@@ -482,6 +484,8 @@ const common = {
|
||||
torrentSeedTimeInvalid: 'Torrent seed time must be greater than zero',
|
||||
torrentSeedRatioInvalid: 'Torrent seed ratio must be zero or greater',
|
||||
torrentUploadLimitInvalid: 'Torrent upload limit must be greater than zero',
|
||||
torrentVerifyIntegrity: 'Verify Torrent integrity',
|
||||
torrentVerifyIntegrityHint: 'Recheck piece hashes when starting or retrying; damaged pieces may be downloaded again.',
|
||||
torrentMaxPeers: 'Maximum Torrent peers',
|
||||
torrentPeerSpeedLimit: 'Peer speed threshold',
|
||||
torrentPeerOptionsHint: 'Leave blank for Aria2 defaults (55 peers and 50K). 0 peers means unlimited.',
|
||||
|
||||
@@ -236,6 +236,8 @@ const fa = {
|
||||
liveTorrentPeerOptionsApply: 'اعمال کنترل همتا',
|
||||
liveTorrentPeerOptionsHint: 'بدون جایگزینی تورنت فعال اعمال میشود. برای استفاده از پیشفرض آریا۲ خالی بگذارید.',
|
||||
torrentPeerOptionsSavedHint: 'برای هر تورنت ذخیره میشود. صفر یعنی نامحدود؛ خالی یعنی پیشفرض آریا۲.',
|
||||
torrentVerifyIntegrity: 'بررسی صحت تورنت',
|
||||
torrentVerifyIntegrityHint: 'هنگام شروع یا تلاش مجدد این تورنت اعمال میشود. ممکن است قطعهها دوباره بررسی و دادههای خراب دوباره دانلود شوند؛ در انتقال فعال قابل تغییر نیست.',
|
||||
torrentMaxPeers: 'حداکثر همتاهای تورنت',
|
||||
torrentPeerSpeedLimit: 'آستانه سرعت همتا',
|
||||
torrentMaxPeersInvalid: 'حداکثر همتاهای تورنت باید عددی صحیح بین ۰ و ۱۰۰۰ باشد',
|
||||
@@ -482,6 +484,8 @@ const fa = {
|
||||
torrentSeedTimeInvalid: 'مدت سید تورنت باید بیشتر از صفر باشد',
|
||||
torrentSeedRatioInvalid: 'نسبت سید تورنت نمیتواند منفی باشد',
|
||||
torrentUploadLimitInvalid: 'محدودیت آپلود تورنت باید بیشتر از صفر باشد',
|
||||
torrentVerifyIntegrity: 'بررسی صحت تورنت',
|
||||
torrentVerifyIntegrityHint: 'هنگام شروع یا تلاش مجدد، هش قطعهها را بررسی میکند؛ قطعههای خراب ممکن است دوباره دانلود شوند.',
|
||||
torrentMaxPeers: 'حداکثر همتاهای تورنت',
|
||||
torrentPeerSpeedLimit: 'آستانه سرعت همتا',
|
||||
torrentPeerOptionsHint: 'برای استفاده از پیشفرضهای آریا۲ خالی بگذارید (۵۵ همتا و 50K). صفر یعنی نامحدود.',
|
||||
|
||||
@@ -236,6 +236,8 @@ const he = {
|
||||
liveTorrentPeerOptionsApply: 'החל בקרות עמיתים',
|
||||
liveTorrentPeerOptionsHint: 'השינוי חל בלי להחליף את הטורנט הפעיל. השאר ריק כדי להשתמש בברירות המחדל של Aria2.',
|
||||
torrentPeerOptionsSavedHint: 'נשמר לכל טורנט. אפס עמיתים פירושו ללא הגבלה; ריק משתמש בברירות המחדל של Aria2.',
|
||||
torrentVerifyIntegrity: 'אימות תקינות הטורנט',
|
||||
torrentVerifyIntegrityHint: 'מוחל כשהטורנט מתחיל או מנסה שוב. ייתכן שהחלקים ייבדקו מחדש ונתונים פגומים יורדו שוב; אי אפשר לשנות זאת בהעברה פעילה.',
|
||||
torrentMaxPeers: 'מספר העמיתים המרבי בטורנט',
|
||||
torrentPeerSpeedLimit: 'סף מהירות עמיתים',
|
||||
torrentMaxPeersInvalid: 'מספר העמיתים המרבי חייב להיות מספר שלם בין 0 ל-1000',
|
||||
@@ -482,6 +484,8 @@ const he = {
|
||||
torrentSeedTimeInvalid: 'זמן שיתוף הטורנט חייב להיות גדול מאפס',
|
||||
torrentSeedRatioInvalid: 'יחס שיתוף הטורנט חייב להיות אפס או יותר',
|
||||
torrentUploadLimitInvalid: 'מגבלת העלאת הטורנט חייבת להיות גדולה מאפס',
|
||||
torrentVerifyIntegrity: 'אימות תקינות הטורנט',
|
||||
torrentVerifyIntegrityHint: 'בדיקת גיבובי החלקים בעת התחלה או ניסיון חוזר; חלקים פגומים עשויים להיות מורדים מחדש.',
|
||||
torrentMaxPeers: 'מספר העמיתים המרבי בטורנט',
|
||||
torrentPeerSpeedLimit: 'סף מהירות עמיתים',
|
||||
torrentPeerOptionsHint: 'השאר ריק כדי להשתמש בברירות המחדל של Aria2 (55 עמיתים ו-50K). אפס עמיתים פירושו ללא הגבלה.',
|
||||
|
||||
@@ -236,6 +236,8 @@ const ru = {
|
||||
liveTorrentPeerOptionsApply: 'Применить настройки пиров',
|
||||
liveTorrentPeerOptionsHint: 'Применяется без замены активного торрента. Оставьте пустым для параметров Aria2 по умолчанию.',
|
||||
torrentPeerOptionsSavedHint: 'Сохраняется для этого торрента. 0 пиров означает без ограничений; пустое поле использует настройки Aria2 по умолчанию.',
|
||||
torrentVerifyIntegrity: 'Проверять целостность торрента',
|
||||
torrentVerifyIntegrityHint: 'Применяется при запуске или повторной попытке. Может повторно проверить части и скачать повреждённые данные; во время активной передачи изменить нельзя.',
|
||||
torrentMaxPeers: 'Максимум пиров торрента',
|
||||
torrentPeerSpeedLimit: 'Порог скорости пиров',
|
||||
torrentMaxPeersInvalid: 'Максимум пиров должен быть целым числом от 0 до 1000',
|
||||
@@ -482,6 +484,8 @@ const ru = {
|
||||
torrentSeedTimeInvalid: 'Время раздачи торрента должно быть больше нуля',
|
||||
torrentSeedRatioInvalid: 'Коэффициент раздачи торрента не может быть отрицательным',
|
||||
torrentUploadLimitInvalid: 'Лимит отдачи торрента должен быть больше нуля',
|
||||
torrentVerifyIntegrity: 'Проверять целостность торрента',
|
||||
torrentVerifyIntegrityHint: 'Проверка хешей частей при запуске или повторной попытке; повреждённые части могут быть загружены заново.',
|
||||
torrentMaxPeers: 'Максимум пиров торрента',
|
||||
torrentPeerSpeedLimit: 'Порог скорости пиров',
|
||||
torrentPeerOptionsHint: 'Оставьте пустым для параметров Aria2 по умолчанию (55 пиров и 50K). 0 пиров означает без ограничений.',
|
||||
|
||||
@@ -236,6 +236,8 @@ const uk = {
|
||||
liveTorrentPeerOptionsApply: 'Застосувати налаштування пірів',
|
||||
liveTorrentPeerOptionsHint: 'Застосовується без заміни активного торрента. Залиште порожнім для стандартних параметрів Aria2.',
|
||||
torrentPeerOptionsSavedHint: 'Зберігається для цього торрента. 0 пірів означає без обмежень; порожнє поле використовує стандартні параметри Aria2.',
|
||||
torrentVerifyIntegrity: 'Перевіряти цілісність торрента',
|
||||
torrentVerifyIntegrityHint: 'Застосовується під час запуску або повторної спроби. Частини можуть перевірятися повторно, а пошкоджені дані — завантажуватися знову; під час активної передачі змінити не можна.',
|
||||
torrentMaxPeers: 'Максимум пірів торрента',
|
||||
torrentPeerSpeedLimit: 'Поріг швидкості пірів',
|
||||
torrentMaxPeersInvalid: 'Максимум пірів має бути цілим числом від 0 до 1000',
|
||||
@@ -482,6 +484,8 @@ const uk = {
|
||||
torrentSeedTimeInvalid: 'Час роздачі торрента має бути більшим за нуль',
|
||||
torrentSeedRatioInvalid: 'Коефіцієнт роздачі торрента не може бути від’ємним',
|
||||
torrentUploadLimitInvalid: 'Ліміт віддачі торрента має бути більшим за нуль',
|
||||
torrentVerifyIntegrity: 'Перевіряти цілісність торрента',
|
||||
torrentVerifyIntegrityHint: 'Перевіряє хеші частин під час запуску або повторної спроби; пошкоджені частини можуть завантажуватися знову.',
|
||||
torrentMaxPeers: 'Максимум пірів торрента',
|
||||
torrentPeerSpeedLimit: 'Поріг швидкості пірів',
|
||||
torrentPeerOptionsHint: 'Залиште порожнім для стандартних параметрів Aria2 (55 пірів і 50K). 0 пірів означає без обмежень.',
|
||||
|
||||
@@ -236,6 +236,8 @@ const zhCN = {
|
||||
liveTorrentPeerOptionsApply: '应用节点控制',
|
||||
liveTorrentPeerOptionsHint: '无需替换活动 Torrent 即可应用。留空以使用 Aria2 默认值。',
|
||||
torrentPeerOptionsSavedHint: '按 Torrent 保存。0 个节点表示不限制;留空使用 Aria2 默认值。',
|
||||
torrentVerifyIntegrity: '验证 Torrent 完整性',
|
||||
torrentVerifyIntegrityHint: '在 Torrent 启动或重试时应用。可能会重新检查分片并重新下载损坏的数据;活动传输期间无法更改。',
|
||||
torrentMaxPeers: 'Torrent 最大对等节点数',
|
||||
torrentPeerSpeedLimit: '对等节点速度阈值',
|
||||
torrentMaxPeersInvalid: 'Torrent 最大对等节点数必须是 0 到 1000 之间的整数',
|
||||
@@ -482,6 +484,8 @@ const zhCN = {
|
||||
torrentSeedTimeInvalid: '做种时间必须大于零',
|
||||
torrentSeedRatioInvalid: '做种比率不能小于零',
|
||||
torrentUploadLimitInvalid: '种子上传限速必须大于零',
|
||||
torrentVerifyIntegrity: '验证 Torrent 完整性',
|
||||
torrentVerifyIntegrityHint: '启动或重试时重新检查分片哈希;损坏的分片可能会再次下载。',
|
||||
torrentMaxPeers: 'Torrent 最大对等节点数',
|
||||
torrentPeerSpeedLimit: '对等节点速度阈值',
|
||||
torrentPeerOptionsHint: '留空以使用 Aria2 默认值(55 个节点和 50K)。0 个节点表示不限制。',
|
||||
|
||||
@@ -852,11 +852,13 @@ describe('useDownloadStore', () => {
|
||||
dateAdded: '',
|
||||
isTorrent: true,
|
||||
torrentMaxPeers: 'not-a-number' as unknown as number,
|
||||
torrentPeerSpeedLimit: 0 as unknown as string
|
||||
torrentPeerSpeedLimit: 0 as unknown as string,
|
||||
torrentCheckIntegrity: 'yes' as unknown as boolean
|
||||
});
|
||||
|
||||
expect(normalized.torrentMaxPeers).toBeUndefined();
|
||||
expect(normalized.torrentPeerSpeedLimit).toBeUndefined();
|
||||
expect(normalized.torrentCheckIntegrity).toBeUndefined();
|
||||
});
|
||||
|
||||
it('normalizes proxy settings for download dispatch', async () => {
|
||||
@@ -1505,7 +1507,9 @@ describe('useDownloadStore', () => {
|
||||
url: 'https://example.com/start.bin',
|
||||
fileName: 'start.bin',
|
||||
category: 'Other',
|
||||
dateAdded: ''
|
||||
dateAdded: '',
|
||||
isTorrent: true,
|
||||
torrentCheckIntegrity: true
|
||||
}, { type: 'start-now' });
|
||||
|
||||
const item = useDownloadStore.getState().downloads[0];
|
||||
@@ -1514,7 +1518,10 @@ describe('useDownloadStore', () => {
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
||||
'enqueue_download',
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({ id: 'start-1' })
|
||||
item: expect.objectContaining({
|
||||
id: 'start-1',
|
||||
torrent_check_integrity: true
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -350,6 +350,7 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
|
||||
torrent_upload_limit: item.torrentUploadLimit || undefined,
|
||||
torrent_max_peers: item.torrentMaxPeers,
|
||||
torrent_peer_speed_limit: item.torrentPeerSpeedLimit || undefined,
|
||||
torrent_check_integrity: item.torrentCheckIntegrity,
|
||||
lifecycle_generation: lifecycleGeneration.toString(),
|
||||
};
|
||||
|
||||
@@ -626,12 +627,18 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
||||
const normalizedPeerSpeedLimit = typeof rawPeerSpeedLimit === 'string'
|
||||
? normalizeSpeedLimitForBackend(rawPeerSpeedLimit) || undefined
|
||||
: undefined;
|
||||
const rawCheckIntegrity = download.torrentCheckIntegrity as unknown;
|
||||
const normalizedCheckIntegrity = typeof rawCheckIntegrity === 'boolean'
|
||||
? rawCheckIntegrity
|
||||
: undefined;
|
||||
const normalizedOptions = rawMaxPeers !== normalizedMaxPeers ||
|
||||
rawPeerSpeedLimit !== normalizedPeerSpeedLimit
|
||||
rawPeerSpeedLimit !== normalizedPeerSpeedLimit ||
|
||||
rawCheckIntegrity !== normalizedCheckIntegrity
|
||||
? {
|
||||
...download,
|
||||
torrentMaxPeers: normalizedMaxPeers,
|
||||
torrentPeerSpeedLimit: normalizedPeerSpeedLimit
|
||||
torrentPeerSpeedLimit: normalizedPeerSpeedLimit,
|
||||
torrentCheckIntegrity: normalizedCheckIntegrity
|
||||
}
|
||||
: download;
|
||||
|
||||
@@ -2162,6 +2169,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
torrent_upload_limit: item.torrentUploadLimit || undefined,
|
||||
torrent_max_peers: item.torrentMaxPeers,
|
||||
torrent_peer_speed_limit: item.torrentPeerSpeedLimit || undefined,
|
||||
torrent_check_integrity: item.torrentCheckIntegrity,
|
||||
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface AddDownloadDraftRow {
|
||||
torrentUploadLimit?: string;
|
||||
torrentMaxPeers?: number;
|
||||
torrentPeerSpeedLimit?: string;
|
||||
torrentCheckIntegrity?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user