mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-31 13:08:17 +00:00
feat(torrents): add encryption policy
This commit is contained in:
+10
-9
@@ -37,6 +37,10 @@ belong in the download UI. The Aria2 reference is the [1.37.0 manual](https://ar
|
|||||||
- Optional `bt-prioritize-piece` preview policy for the head, tail, or both
|
- Optional `bt-prioritize-piece` preview policy for the head, tail, or both
|
||||||
ends of every selected file. The constrained policy is validated, persisted,
|
ends of every selected file. The constrained policy is validated, persisted,
|
||||||
normalized, and reapplied when a Torrent starts or retries.
|
normalized, and reapplied when a Torrent starts or retries.
|
||||||
|
- One validated Torrent encryption policy mapped to Aria2's
|
||||||
|
`bt-force-encryption`, `bt-require-crypto`, and `bt-min-crypto-level`:
|
||||||
|
disabled, required obfuscated handshake, or forced ARC4 payload encryption.
|
||||||
|
The policy is persisted and reapplied when a Torrent starts or retries.
|
||||||
- Optional `bt-remove-unselected-file` cleanup after completion when a
|
- Optional `bt-remove-unselected-file` cleanup after completion when a
|
||||||
selected-file subset is configured. Firelink requires explicit confirmation,
|
selected-file subset is configured. Firelink requires explicit confirmation,
|
||||||
reserves the unselected paths against competing downloads, keeps those
|
reserves the unselected paths against competing downloads, keeps those
|
||||||
@@ -44,9 +48,9 @@ belong in the download UI. The Aria2 reference is the [1.37.0 manual](https://ar
|
|||||||
after observing Aria2's completion cleanup (or on terminal failure,
|
after observing Aria2's completion cleanup (or on terminal failure,
|
||||||
cancellation, or reconfiguration).
|
cancellation, or reconfiguration).
|
||||||
- Deterministic local Aria2 smoke coverage for metadata resolution, selected
|
- Deterministic local Aria2 smoke coverage for metadata resolution, selected
|
||||||
output, piece priority, pause/resume, ownership, cancellation/removal,
|
output, piece priority, encryption policy, pause/resume, ownership,
|
||||||
unavailable trackers, daemon failure, and `bt-stop-timeout` terminal behavior;
|
cancellation/removal, unavailable trackers, daemon failure, and
|
||||||
RPC-boundary coverage is separate.
|
`bt-stop-timeout` terminal behavior; RPC-boundary coverage is separate.
|
||||||
|
|
||||||
## Priority tiers for remaining work
|
## Priority tiers for remaining work
|
||||||
|
|
||||||
@@ -61,10 +65,7 @@ No remaining Tier 0 items.
|
|||||||
per-file priority option. Firelink therefore does not pretend that
|
per-file priority option. Firelink therefore does not pretend that
|
||||||
`select-file` is file priority; this remains pending an engine capability or
|
`select-file` is file priority; this remains pending an engine capability or
|
||||||
a safe product-level model.
|
a safe product-level model.
|
||||||
2. **Encryption policy** — expose `bt-force-encryption`,
|
2. **Tracker timing controls** — expose tracker connect timeout, request
|
||||||
`bt-require-crypto`, and `bt-min-crypto-level` as one validated policy so
|
|
||||||
users cannot accidentally select contradictory combinations.
|
|
||||||
3. **Tracker timing controls** — expose tracker connect timeout, request
|
|
||||||
timeout, and interval only when their effect on battery/network behavior is
|
timeout, and interval only when their effect on battery/network behavior is
|
||||||
explained and persisted.
|
explained and persisted.
|
||||||
|
|
||||||
@@ -79,5 +80,5 @@ No remaining Tier 0 items.
|
|||||||
|
|
||||||
The first implementation in this task was remote `.torrent` metadata intake;
|
The first implementation in this task was remote `.torrent` metadata intake;
|
||||||
follow-up implementations add stall-timeout control, bounded peer diagnostics,
|
follow-up implementations add stall-timeout control, bounded peer diagnostics,
|
||||||
persisted tracker exclusion, piece-preview priority, and safe unselected-file
|
persisted tracker exclusion, piece-preview priority, safe unselected-file
|
||||||
removal.
|
removal, and the validated encryption policy.
|
||||||
|
|||||||
@@ -628,10 +628,11 @@ async function main() {
|
|||||||
const probeDir = path.join(tempRoot, 'probe');
|
const probeDir = path.join(tempRoot, 'probe');
|
||||||
const finalDir = path.join(tempRoot, 'final');
|
const finalDir = path.join(tempRoot, 'final');
|
||||||
const integrityDir = path.join(tempRoot, 'integrity');
|
const integrityDir = path.join(tempRoot, 'integrity');
|
||||||
|
const encryptionDir = path.join(tempRoot, 'encryption');
|
||||||
const cancelDir = path.join(tempRoot, 'cancel');
|
const cancelDir = path.join(tempRoot, 'cancel');
|
||||||
const removeUnselectedDir = path.join(tempRoot, 'remove-unselected');
|
const removeUnselectedDir = path.join(tempRoot, 'remove-unselected');
|
||||||
const stallDir = path.join(tempRoot, 'stall');
|
const stallDir = path.join(tempRoot, 'stall');
|
||||||
for (const directory of [seedRoot, probeDir, finalDir, integrityDir, cancelDir, removeUnselectedDir, stallDir]) fs.mkdirSync(directory, { recursive: true });
|
for (const directory of [seedRoot, probeDir, finalDir, integrityDir, encryptionDir, cancelDir, removeUnselectedDir, stallDir]) fs.mkdirSync(directory, { recursive: true });
|
||||||
|
|
||||||
const seederListenPort = await findAvailablePort();
|
const seederListenPort = await findAvailablePort();
|
||||||
const clientListenPort = await findAvailablePort();
|
const clientListenPort = await findAvailablePort();
|
||||||
@@ -765,6 +766,29 @@ async function main() {
|
|||||||
assert(fs.readFileSync(integrityPath).equals(torrent.files[0].data), 'integrity check did not replace corrupted torrent data');
|
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');
|
console.log('[OK] check-integrity detected and repaired corrupted Torrent data');
|
||||||
|
|
||||||
|
const encryptionGid = await rpc(client.rpcPort, client.secret, 'aria2.addTorrent', [
|
||||||
|
savedTorrentBytes.toString('base64'),
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
dir: encryptionDir,
|
||||||
|
'select-file': '1',
|
||||||
|
'index-out': indexOut,
|
||||||
|
'bt-force-encryption': 'true',
|
||||||
|
'bt-require-crypto': 'true',
|
||||||
|
'bt-min-crypto-level': 'arc4',
|
||||||
|
'seed-time': '0',
|
||||||
|
'auto-file-renaming': 'false',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const encryptionOptions = await rpc(client.rpcPort, client.secret, 'aria2.getOption', [encryptionGid]);
|
||||||
|
assert(encryptionOptions['bt-force-encryption'] === 'true', 'Aria2 did not retain force encryption');
|
||||||
|
assert(encryptionOptions['bt-require-crypto'] === 'true', 'Aria2 did not retain the crypto requirement');
|
||||||
|
assert(encryptionOptions['bt-min-crypto-level'] === 'arc4', 'Aria2 did not retain the ARC4 minimum crypto level');
|
||||||
|
await waitForTerminal(client, encryptionGid, 30000);
|
||||||
|
const encryptedPath = path.join(encryptionDir, torrent.name, 'selected.bin');
|
||||||
|
assert(fs.readFileSync(encryptedPath).equals(torrent.files[0].data), 'encrypted Torrent output content differs');
|
||||||
|
console.log('[OK] Torrent encryption policy retained all three Aria2 options and completed');
|
||||||
|
|
||||||
const removalSkippedPath = path.join(removeUnselectedDir, torrent.name, 'skipped.bin');
|
const removalSkippedPath = path.join(removeUnselectedDir, torrent.name, 'skipped.bin');
|
||||||
fs.mkdirSync(path.dirname(removalSkippedPath), { recursive: true });
|
fs.mkdirSync(path.dirname(removalSkippedPath), { recursive: true });
|
||||||
fs.writeFileSync(removalSkippedPath, Buffer.from('pre-existing file owned outside the Torrent\n'));
|
fs.writeFileSync(removalSkippedPath, Buffer.from('pre-existing file owned outside the Torrent\n'));
|
||||||
|
|||||||
@@ -209,6 +209,9 @@ pub struct DownloadItem {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
pub torrent_remove_unselected_file: Option<bool>,
|
pub torrent_remove_unselected_file: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[ts(optional)]
|
||||||
|
pub torrent_encryption_policy: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, TS)]
|
#[derive(Clone, Debug, Serialize, TS)]
|
||||||
|
|||||||
@@ -5806,6 +5806,9 @@ async fn validate_torrent_enqueue(
|
|||||||
item.torrent_prioritize_piece = queue::normalize_torrent_prioritize_piece(
|
item.torrent_prioritize_piece = queue::normalize_torrent_prioritize_piece(
|
||||||
item.torrent_prioritize_piece.as_deref(),
|
item.torrent_prioritize_piece.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
|
item.torrent_encryption_policy = queue::normalize_torrent_encryption_policy(
|
||||||
|
item.torrent_encryption_policy.as_deref(),
|
||||||
|
)?;
|
||||||
validate_enqueue_uris("", item.mirrors.as_deref()).await?;
|
validate_enqueue_uris("", item.mirrors.as_deref()).await?;
|
||||||
if let Some(path) = item.torrent_path.as_deref() {
|
if let Some(path) = item.torrent_path.as_deref() {
|
||||||
let path = crate::torrent::validate_managed_torrent_path(app_handle, &item.id, path)?;
|
let path = crate::torrent::validate_managed_torrent_path(app_handle, &item.id, path)?;
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ pub struct SpawnPayload {
|
|||||||
pub torrent_stop_timeout: Option<u32>,
|
pub torrent_stop_timeout: Option<u32>,
|
||||||
pub torrent_prioritize_piece: Option<String>,
|
pub torrent_prioritize_piece: Option<String>,
|
||||||
pub torrent_remove_unselected_file: bool,
|
pub torrent_remove_unselected_file: bool,
|
||||||
|
pub torrent_encryption_policy: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sidecar spawner. In production this calls the real aria2/yt-dlp
|
/// A sidecar spawner. In production this calls the real aria2/yt-dlp
|
||||||
@@ -3506,6 +3507,23 @@ pub(crate) fn normalize_torrent_exclude_trackers(
|
|||||||
normalize_torrent_tracker_list(value, true)
|
normalize_torrent_tracker_list(value, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn normalize_torrent_encryption_policy(
|
||||||
|
value: Option<&str>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
let Some(raw) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
match raw {
|
||||||
|
"disabled" => Ok(None),
|
||||||
|
"require-crypto" => Ok(Some("require-crypto".to_string())),
|
||||||
|
"force-encryption" => Ok(Some("force-encryption".to_string())),
|
||||||
|
_ => Err(
|
||||||
|
"torrent encryption policy must be disabled, require-crypto, or force-encryption"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_aria2_torrent_options(
|
fn apply_aria2_torrent_options(
|
||||||
options: &mut serde_json::Map<String, serde_json::Value>,
|
options: &mut serde_json::Map<String, serde_json::Value>,
|
||||||
payload: &SpawnPayload,
|
payload: &SpawnPayload,
|
||||||
@@ -3514,6 +3532,30 @@ fn apply_aria2_torrent_options(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let encryption_policy =
|
||||||
|
normalize_torrent_encryption_policy(payload.torrent_encryption_policy.as_deref())?;
|
||||||
|
let (force_encryption, require_crypto, min_crypto_level) =
|
||||||
|
match encryption_policy.as_deref() {
|
||||||
|
Some("require-crypto") => (false, true, "plain"),
|
||||||
|
Some("force-encryption") => (true, true, "arc4"),
|
||||||
|
None => (false, false, "plain"),
|
||||||
|
Some(policy) => {
|
||||||
|
return Err(format!("unsupported normalized Torrent encryption policy: {policy}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options.insert(
|
||||||
|
"bt-force-encryption".to_string(),
|
||||||
|
serde_json::json!(force_encryption.to_string()),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"bt-require-crypto".to_string(),
|
||||||
|
serde_json::json!(require_crypto.to_string()),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"bt-min-crypto-level".to_string(),
|
||||||
|
serde_json::json!(min_crypto_level),
|
||||||
|
);
|
||||||
|
|
||||||
let seed_time = payload
|
let seed_time = payload
|
||||||
.torrent_seed_time
|
.torrent_seed_time
|
||||||
.map(|value| format_aria2_torrent_number(value, "seed time"))
|
.map(|value| format_aria2_torrent_number(value, "seed time"))
|
||||||
@@ -4224,6 +4266,9 @@ pub struct EnqueueItem {
|
|||||||
pub torrent_remove_unselected_file: Option<bool>,
|
pub torrent_remove_unselected_file: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[ts(optional)]
|
#[ts(optional)]
|
||||||
|
pub torrent_encryption_policy: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[ts(optional)]
|
||||||
pub lifecycle_generation: Option<String>,
|
pub lifecycle_generation: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4279,6 +4324,7 @@ impl EnqueueItem {
|
|||||||
torrent_remove_unselected_file: self
|
torrent_remove_unselected_file: self
|
||||||
.torrent_remove_unselected_file
|
.torrent_remove_unselected_file
|
||||||
.unwrap_or(false),
|
.unwrap_or(false),
|
||||||
|
torrent_encryption_policy: self.torrent_encryption_policy,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4343,6 +4389,87 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(options.get("seed-time"), Some(&serde_json::json!("0")));
|
assert_eq!(options.get("seed-time"), Some(&serde_json::json!("0")));
|
||||||
assert!(!options.contains_key("max-upload-limit"));
|
assert!(!options.contains_key("max-upload-limit"));
|
||||||
|
assert_eq!(
|
||||||
|
options.get("bt-force-encryption"),
|
||||||
|
Some(&serde_json::json!("false"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.get("bt-require-crypto"),
|
||||||
|
Some(&serde_json::json!("false"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.get("bt-min-crypto-level"),
|
||||||
|
Some(&serde_json::json!("plain"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn torrent_encryption_policy_maps_to_one_consistent_aria2_policy() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
Some("require-crypto"),
|
||||||
|
("false", "true", "plain"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Some("force-encryption"),
|
||||||
|
("true", "true", "arc4"),
|
||||||
|
),
|
||||||
|
(None, ("false", "false", "plain")),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (policy, expected) in cases {
|
||||||
|
let mut options = serde_json::Map::new();
|
||||||
|
let payload = SpawnPayload {
|
||||||
|
is_torrent: true,
|
||||||
|
torrent_encryption_policy: policy.map(str::to_string),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
options.get("bt-force-encryption"),
|
||||||
|
Some(&serde_json::json!(expected.0))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.get("bt-require-crypto"),
|
||||||
|
Some(&serde_json::json!(expected.1))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.get("bt-min-crypto-level"),
|
||||||
|
Some(&serde_json::json!(expected.2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn torrent_encryption_policy_rejects_unknown_values() {
|
||||||
|
assert_eq!(normalize_torrent_encryption_policy(None).unwrap(), None);
|
||||||
|
assert_eq!(
|
||||||
|
normalize_torrent_encryption_policy(Some(" disabled ")).unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
normalize_torrent_encryption_policy(Some("require-crypto")).unwrap(),
|
||||||
|
Some("require-crypto".to_string())
|
||||||
|
);
|
||||||
|
assert!(normalize_torrent_encryption_policy(Some("arc4")).is_err());
|
||||||
|
assert!(normalize_torrent_encryption_policy(Some("true")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn torrent_encryption_policy_is_not_applied_to_non_torrent_payloads() {
|
||||||
|
let mut options = serde_json::Map::new();
|
||||||
|
let payload = SpawnPayload {
|
||||||
|
torrent_encryption_policy: Some("force-encryption".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||||
|
|
||||||
|
assert!(!options.contains_key("bt-force-encryption"));
|
||||||
|
assert!(!options.contains_key("bt-require-crypto"));
|
||||||
|
assert!(!options.contains_key("bt-min-crypto-level"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4472,6 +4599,26 @@ mod tests {
|
|||||||
assert!(item.into_task().payload.torrent_check_integrity);
|
assert!(item.into_task().payload.torrent_check_integrity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enqueue_item_preserves_torrent_encryption_policy() {
|
||||||
|
let item: EnqueueItem = serde_json::from_value(serde_json::json!({
|
||||||
|
"id": "torrent-encryption",
|
||||||
|
"queue_id": "main",
|
||||||
|
"url": "magnet:?xt=urn:btih:0123456789012345678901234567890123456789",
|
||||||
|
"destination": "/tmp/downloads",
|
||||||
|
"filename": "payload",
|
||||||
|
"is_media": false,
|
||||||
|
"is_torrent": true,
|
||||||
|
"torrent_encryption_policy": "force-encryption"
|
||||||
|
}))
|
||||||
|
.expect("frontend enqueue payload should deserialize");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
item.into_task().payload.torrent_encryption_policy.as_deref(),
|
||||||
|
Some("force-encryption")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn torrent_trackers_are_normalized_and_deduplicated() {
|
fn torrent_trackers_are_normalized_and_deduplicated() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
import type { DownloadCategory } from "./DownloadCategory";
|
import type { DownloadCategory } from "./DownloadCategory";
|
||||||
import type { DownloadStatus } from "./DownloadStatus";
|
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, torrentCheckIntegrity?: boolean, torrentTrackers?: string, torrentExcludeTrackers?: string, torrentStopTimeout?: number, torrentPrioritizePiece?: string, torrentRemoveUnselectedFile?: boolean, };
|
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, torrentTrackers?: string, torrentExcludeTrackers?: string, torrentStopTimeout?: number, torrentPrioritizePiece?: string, torrentRemoveUnselectedFile?: boolean, torrentEncryptionPolicy?: string, };
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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, torrent_check_integrity?: boolean, torrent_trackers?: string, torrent_exclude_trackers?: string, torrent_stop_timeout?: number, torrent_prioritize_piece?: string, torrent_remove_unselected_file?: boolean, 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, torrent_trackers?: string, torrent_exclude_trackers?: string, torrent_stop_timeout?: number, torrent_prioritize_piece?: string, torrent_remove_unselected_file?: boolean, torrent_encryption_policy?: string, lifecycle_generation?: string, };
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { FolderPlus, Save, Settings, Shield, RefreshCw, FileText, HardDrive, Dat
|
|||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { invokeCommand as invoke } from '../ipc';
|
import { invokeCommand as invoke } from '../ipc';
|
||||||
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
|
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
|
||||||
import { canonicalizeDownloadFileName, categoryForFileName, downloadFileNameWithSuffix, downloadFileNamesMatch, downloadMediaKindsMatch, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentPrioritizePiece } from '../utils/downloads';
|
import { canonicalizeDownloadFileName, categoryForFileName, downloadFileNameWithSuffix, downloadFileNamesMatch, downloadMediaKindsMatch, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentPrioritizePiece, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy } from '../utils/downloads';
|
||||||
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
||||||
import {
|
import {
|
||||||
expandTilde,
|
expandTilde,
|
||||||
@@ -234,6 +234,7 @@ export const AddDownloadsModal = () => {
|
|||||||
const [torrentPeerSpeedLimit, setTorrentPeerSpeedLimit] = useState('');
|
const [torrentPeerSpeedLimit, setTorrentPeerSpeedLimit] = useState('');
|
||||||
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
||||||
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
|
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
|
||||||
|
const [torrentEncryptionPolicy, setTorrentEncryptionPolicy] = useState<TorrentEncryptionPolicy>(TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||||
const [torrentTrackers, setTorrentTrackers] = useState('');
|
const [torrentTrackers, setTorrentTrackers] = useState('');
|
||||||
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
||||||
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
|
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
|
||||||
@@ -1495,6 +1496,9 @@ export const AddDownloadsModal = () => {
|
|||||||
torrentRemoveUnselectedFile: item.isTorrent && torrentRemoveUnselectedFile && hasPartialTorrentSelection(item)
|
torrentRemoveUnselectedFile: item.isTorrent && torrentRemoveUnselectedFile && hasPartialTorrentSelection(item)
|
||||||
? true
|
? true
|
||||||
: undefined,
|
: undefined,
|
||||||
|
torrentEncryptionPolicy: item.isTorrent && torrentEncryptionPolicy !== TORRENT_ENCRYPTION_POLICY_DISABLED
|
||||||
|
? torrentEncryptionPolicy
|
||||||
|
: undefined,
|
||||||
torrentTrackers: item.isTorrent ? torrentTrackers.trim() || undefined : undefined,
|
torrentTrackers: item.isTorrent ? torrentTrackers.trim() || undefined : undefined,
|
||||||
torrentExcludeTrackers: item.isTorrent ? torrentExcludeTrackers.trim() || undefined : undefined,
|
torrentExcludeTrackers: item.isTorrent ? torrentExcludeTrackers.trim() || undefined : undefined,
|
||||||
torrentStopTimeout: item.isTorrent && torrentStopTimeout.trim() ? Number(torrentStopTimeout) : undefined,
|
torrentStopTimeout: item.isTorrent && torrentStopTimeout.trim() ? Number(torrentStopTimeout) : undefined,
|
||||||
@@ -2164,6 +2168,31 @@ export const AddDownloadsModal = () => {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div className="grid grid-cols-[1fr_auto] gap-2 items-center pt-2 border-t border-border-modal/50">
|
||||||
|
<label htmlFor="torrent-encryption-policy" className="text-text-muted">
|
||||||
|
{t($ => $.addDownloads.torrentEncryptionPolicy)}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="torrent-encryption-policy"
|
||||||
|
value={torrentEncryptionPolicy}
|
||||||
|
onChange={event => setTorrentEncryptionPolicy(event.currentTarget.value as TorrentEncryptionPolicy)}
|
||||||
|
aria-describedby="torrent-encryption-policy-hint"
|
||||||
|
className="app-control max-w-56 px-2 py-1 text-xs"
|
||||||
|
>
|
||||||
|
<option value={TORRENT_ENCRYPTION_POLICY_DISABLED}>
|
||||||
|
{t($ => $.addDownloads.torrentEncryptionDisabled)}
|
||||||
|
</option>
|
||||||
|
<option value={TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO}>
|
||||||
|
{t($ => $.addDownloads.torrentEncryptionRequireCrypto)}
|
||||||
|
</option>
|
||||||
|
<option value={TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION}>
|
||||||
|
{t($ => $.addDownloads.torrentEncryptionForceEncryption)}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p id="torrent-encryption-policy-hint" className="col-span-2 text-[10px] text-text-muted">
|
||||||
|
{t($ => $.addDownloads.torrentEncryptionPolicyHint)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<label className="flex items-start gap-2 text-text-primary pt-2 border-t border-border-modal/50">
|
<label className="flex items-start gap-2 text-text-primary pt-2 border-t border-border-modal/50">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
formatDownloadTotal,
|
formatDownloadTotal,
|
||||||
resolveDownloadSizeDisplay
|
resolveDownloadSizeDisplay
|
||||||
} from '../utils/downloadProgress';
|
} from '../utils/downloadProgress';
|
||||||
import { isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentPrioritizePiece, resolveDownloadConnections } from '../utils/downloads';
|
import { isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentPrioritizePiece, resolveDownloadConnections, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy } from '../utils/downloads';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { formatDateTime, type CalendarPreference } from '../utils/dateTime';
|
import { formatDateTime, type CalendarPreference } from '../utils/dateTime';
|
||||||
import { isTopmostModal, useModalFocus } from '../hooks/useModalFocus';
|
import { isTopmostModal, useModalFocus } from '../hooks/useModalFocus';
|
||||||
@@ -88,6 +88,7 @@ export const PropertiesModal = () => {
|
|||||||
const [liveTorrentPeerSpeedLimitValue, setLiveTorrentPeerSpeedLimitValue] = useState('');
|
const [liveTorrentPeerSpeedLimitValue, setLiveTorrentPeerSpeedLimitValue] = useState('');
|
||||||
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
||||||
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
|
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
|
||||||
|
const [torrentEncryptionPolicy, setTorrentEncryptionPolicy] = useState<TorrentEncryptionPolicy>(TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||||
const [torrentTrackers, setTorrentTrackers] = useState('');
|
const [torrentTrackers, setTorrentTrackers] = useState('');
|
||||||
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
||||||
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
|
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
|
||||||
@@ -193,6 +194,7 @@ export const PropertiesModal = () => {
|
|||||||
setLiveTorrentPeerSpeedLimitValue(activeItem.torrentPeerSpeedLimit || '');
|
setLiveTorrentPeerSpeedLimitValue(activeItem.torrentPeerSpeedLimit || '');
|
||||||
setTorrentCheckIntegrity(activeItem.torrentCheckIntegrity === true);
|
setTorrentCheckIntegrity(activeItem.torrentCheckIntegrity === true);
|
||||||
setTorrentRemoveUnselectedFile(activeItem.torrentRemoveUnselectedFile === true);
|
setTorrentRemoveUnselectedFile(activeItem.torrentRemoveUnselectedFile === true);
|
||||||
|
setTorrentEncryptionPolicy(normalizeTorrentEncryptionPolicy(activeItem.torrentEncryptionPolicy) || TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||||
setTorrentTrackers(activeItem.torrentTrackers || '');
|
setTorrentTrackers(activeItem.torrentTrackers || '');
|
||||||
setTorrentExcludeTrackers(activeItem.torrentExcludeTrackers || '');
|
setTorrentExcludeTrackers(activeItem.torrentExcludeTrackers || '');
|
||||||
setTorrentStopTimeout(activeItem.torrentStopTimeout === undefined ? '0' : String(activeItem.torrentStopTimeout));
|
setTorrentStopTimeout(activeItem.torrentStopTimeout === undefined ? '0' : String(activeItem.torrentStopTimeout));
|
||||||
@@ -366,6 +368,10 @@ export const PropertiesModal = () => {
|
|||||||
setErrorMessage(t($ => $.properties.torrentRemoveUnselectedFileSelectionRequired));
|
setErrorMessage(t($ => $.properties.torrentRemoveUnselectedFileSelectionRequired));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (item.isTorrent && !normalizeTorrentEncryptionPolicy(torrentEncryptionPolicy)) {
|
||||||
|
setErrorMessage(t($ => $.properties.torrentEncryptionPolicyInvalid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
item.isTorrent
|
item.isTorrent
|
||||||
&& torrentRemoveUnselectedFile
|
&& torrentRemoveUnselectedFile
|
||||||
@@ -398,6 +404,9 @@ export const PropertiesModal = () => {
|
|||||||
torrentRemoveUnselectedFile: item.torrentFileIndices !== undefined
|
torrentRemoveUnselectedFile: item.torrentFileIndices !== undefined
|
||||||
? torrentRemoveUnselectedFile
|
? torrentRemoveUnselectedFile
|
||||||
: undefined,
|
: undefined,
|
||||||
|
torrentEncryptionPolicy: torrentEncryptionPolicy !== TORRENT_ENCRYPTION_POLICY_DISABLED
|
||||||
|
? torrentEncryptionPolicy
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(connectionsDirty
|
...(connectionsDirty
|
||||||
@@ -972,6 +981,32 @@ export const PropertiesModal = () => {
|
|||||||
{t($ => $.properties.torrentPrioritizePieceHint)}
|
{t($ => $.properties.torrentPrioritizePieceHint)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="text-xs text-text-muted text-right" htmlFor="torrent-encryption-policy-properties">
|
||||||
|
{t($ => $.properties.torrentEncryptionPolicy)}
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
id="torrent-encryption-policy-properties"
|
||||||
|
value={torrentEncryptionPolicy}
|
||||||
|
onChange={event => setTorrentEncryptionPolicy(event.currentTarget.value as TorrentEncryptionPolicy)}
|
||||||
|
disabled={transferLocked}
|
||||||
|
aria-describedby="torrent-encryption-policy-properties-hint"
|
||||||
|
className="app-control max-w-56 px-2.5 py-1.5 text-xs disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value={TORRENT_ENCRYPTION_POLICY_DISABLED}>
|
||||||
|
{t($ => $.properties.torrentEncryptionDisabled)}
|
||||||
|
</option>
|
||||||
|
<option value={TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO}>
|
||||||
|
{t($ => $.properties.torrentEncryptionRequireCrypto)}
|
||||||
|
</option>
|
||||||
|
<option value={TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION}>
|
||||||
|
{t($ => $.properties.torrentEncryptionForceEncryption)}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<p id="torrent-encryption-policy-properties-hint" className="mt-1 text-[11px] text-text-muted">
|
||||||
|
{t($ => $.properties.torrentEncryptionPolicyHint)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-check-integrity">
|
<label className="text-xs text-text-muted text-right" htmlFor="torrent-check-integrity">
|
||||||
{t($ => $.properties.torrentVerifyIntegrity)}
|
{t($ => $.properties.torrentVerifyIntegrity)}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ const common = {
|
|||||||
torrentPrioritizePiece: 'Prioritize Torrent pieces',
|
torrentPrioritizePiece: 'Prioritize Torrent pieces',
|
||||||
torrentPrioritizePieceHint: 'Optional Aria2 preview policy: head, tail, or both; each may use a size such as 1M. Changes apply when the Torrent starts or retries.',
|
torrentPrioritizePieceHint: 'Optional Aria2 preview policy: head, tail, or both; each may use a size such as 1M. Changes apply when the Torrent starts or retries.',
|
||||||
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
|
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
|
||||||
|
torrentEncryptionPolicy: 'Torrent encryption policy',
|
||||||
|
torrentEncryptionPolicyHint: 'Applied when this Torrent starts or retries. Choose one policy so the handshake and payload encryption settings stay consistent.',
|
||||||
|
torrentEncryptionDisabled: 'Disabled',
|
||||||
|
torrentEncryptionRequireCrypto: 'Require obfuscated handshake',
|
||||||
|
torrentEncryptionForceEncryption: 'Force encrypted payload (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'Choose a valid Torrent encryption policy',
|
||||||
torrentRemoveUnselectedFile: 'Delete unselected Torrent files after completion',
|
torrentRemoveUnselectedFile: 'Delete unselected Torrent files after completion',
|
||||||
torrentRemoveUnselectedFileHint: 'Only applies when a subset of files is selected. Aria2 permanently deletes the other files after the Torrent completes.',
|
torrentRemoveUnselectedFileHint: 'Only applies when a subset of files is selected. Aria2 permanently deletes the other files after the Torrent completes.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'Delete {{count}} unselected Torrent files after completion? This cannot be undone.',
|
torrentRemoveUnselectedFileConfirm: 'Delete {{count}} unselected Torrent files after completion? This cannot be undone.',
|
||||||
@@ -534,6 +540,12 @@ const common = {
|
|||||||
torrentPrioritizePiece: 'Prioritize Torrent pieces',
|
torrentPrioritizePiece: 'Prioritize Torrent pieces',
|
||||||
torrentPrioritizePieceHint: 'Saved with this Torrent and applied on its next start or retry. Use head, tail, or both with optional K or M sizes.',
|
torrentPrioritizePieceHint: 'Saved with this Torrent and applied on its next start or retry. Use head, tail, or both with optional K or M sizes.',
|
||||||
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
|
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
|
||||||
|
torrentEncryptionPolicy: 'Torrent encryption policy',
|
||||||
|
torrentEncryptionPolicyHint: 'Saved with this Torrent and applied on its next start or retry. The selected policy keeps Aria2 encryption settings consistent.',
|
||||||
|
torrentEncryptionDisabled: 'Disabled',
|
||||||
|
torrentEncryptionRequireCrypto: 'Require obfuscated handshake',
|
||||||
|
torrentEncryptionForceEncryption: 'Force encrypted payload (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'Choose a valid Torrent encryption policy',
|
||||||
torrentRemoveUnselectedFile: 'Delete unselected Torrent files after completion',
|
torrentRemoveUnselectedFile: 'Delete unselected Torrent files after completion',
|
||||||
torrentRemoveUnselectedFileHint: 'Only applies when a selected subset is configured. The unselected files are not Firelink-owned and are permanently removed when the Torrent completes.',
|
torrentRemoveUnselectedFileHint: 'Only applies when a selected subset is configured. The unselected files are not Firelink-owned and are permanently removed when the Torrent completes.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'Enable permanent deletion of unselected Torrent files after completion? This cannot be undone.',
|
torrentRemoveUnselectedFileConfirm: 'Enable permanent deletion of unselected Torrent files after completion? This cannot be undone.',
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ const fa = {
|
|||||||
torrentPrioritizePiece: 'اولویتبندی قطعههای تورنت',
|
torrentPrioritizePiece: 'اولویتبندی قطعههای تورنت',
|
||||||
torrentPrioritizePieceHint: 'سیاست اختیاری پیشنمایش آریا۲: ابتدا، انتها یا هر دو؛ برای هرکدام میتوان اندازهای مثل 1M نوشت. تغییرات هنگام شروع یا تلاش مجدد اعمال میشوند.',
|
torrentPrioritizePieceHint: 'سیاست اختیاری پیشنمایش آریا۲: ابتدا، انتها یا هر دو؛ برای هرکدام میتوان اندازهای مثل 1M نوشت. تغییرات هنگام شروع یا تلاش مجدد اعمال میشوند.',
|
||||||
torrentPrioritizePieceInvalid: 'اولویت قطعههای تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
|
torrentPrioritizePieceInvalid: 'اولویت قطعههای تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
|
||||||
|
torrentEncryptionPolicy: 'سیاست رمزنگاری تورنت',
|
||||||
|
torrentEncryptionPolicyHint: 'هنگام شروع یا تلاش مجدد اعمال میشود. یک سیاست واحد انتخاب کنید تا تنظیمات handshake و رمزنگاری payload آریا۲ سازگار بمانند.',
|
||||||
|
torrentEncryptionDisabled: 'غیرفعال',
|
||||||
|
torrentEncryptionRequireCrypto: 'الزام handshake مبهمسازیشده',
|
||||||
|
torrentEncryptionForceEncryption: 'الزام payload رمزنگاریشده (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'یک سیاست معتبر برای رمزنگاری تورنت انتخاب کنید',
|
||||||
torrentRemoveUnselectedFile: 'حذف فایلهای انتخابنشده تورنت پس از تکمیل',
|
torrentRemoveUnselectedFile: 'حذف فایلهای انتخابنشده تورنت پس از تکمیل',
|
||||||
torrentRemoveUnselectedFileHint: 'فقط وقتی اعمال میشود که زیرمجموعهای از فایلها انتخاب شده باشد. آریا۲ فایلهای دیگر را پس از تکمیل تورنت برای همیشه حذف میکند.',
|
torrentRemoveUnselectedFileHint: 'فقط وقتی اعمال میشود که زیرمجموعهای از فایلها انتخاب شده باشد. آریا۲ فایلهای دیگر را پس از تکمیل تورنت برای همیشه حذف میکند.',
|
||||||
torrentRemoveUnselectedFileConfirm: '{{count}} فایل انتخابنشده تورنت پس از تکمیل حذف شوند؟ این کار قابل بازگشت نیست.',
|
torrentRemoveUnselectedFileConfirm: '{{count}} فایل انتخابنشده تورنت پس از تکمیل حذف شوند؟ این کار قابل بازگشت نیست.',
|
||||||
@@ -534,6 +540,12 @@ const fa = {
|
|||||||
torrentPrioritizePiece: 'اولویتبندی قطعههای تورنت',
|
torrentPrioritizePiece: 'اولویتبندی قطعههای تورنت',
|
||||||
torrentPrioritizePieceHint: 'با این تورنت ذخیره و در شروع یا تلاش مجدد بعدی اعمال میشود. ابتدا، انتها یا هر دو را با اندازه اختیاری K یا M وارد کنید.',
|
torrentPrioritizePieceHint: 'با این تورنت ذخیره و در شروع یا تلاش مجدد بعدی اعمال میشود. ابتدا، انتها یا هر دو را با اندازه اختیاری K یا M وارد کنید.',
|
||||||
torrentPrioritizePieceInvalid: 'اولویت قطعههای تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
|
torrentPrioritizePieceInvalid: 'اولویت قطعههای تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
|
||||||
|
torrentEncryptionPolicy: 'سیاست رمزنگاری تورنت',
|
||||||
|
torrentEncryptionPolicyHint: 'با این تورنت ذخیره و در شروع یا تلاش مجدد بعدی اعمال میشود. سیاست انتخابی تنظیمات رمزنگاری آریا۲ را سازگار نگه میدارد.',
|
||||||
|
torrentEncryptionDisabled: 'غیرفعال',
|
||||||
|
torrentEncryptionRequireCrypto: 'الزام handshake مبهمسازیشده',
|
||||||
|
torrentEncryptionForceEncryption: 'الزام payload رمزنگاریشده (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'یک سیاست معتبر برای رمزنگاری تورنت انتخاب کنید',
|
||||||
torrentRemoveUnselectedFile: 'حذف فایلهای انتخابنشده تورنت پس از تکمیل',
|
torrentRemoveUnselectedFile: 'حذف فایلهای انتخابنشده تورنت پس از تکمیل',
|
||||||
torrentRemoveUnselectedFileHint: 'فقط برای زیرمجموعه انتخابشده اعمال میشود. فایلهای انتخابنشده متعلق به Firelink نیستند و هنگام تکمیل تورنت برای همیشه حذف میشوند.',
|
torrentRemoveUnselectedFileHint: 'فقط برای زیرمجموعه انتخابشده اعمال میشود. فایلهای انتخابنشده متعلق به Firelink نیستند و هنگام تکمیل تورنت برای همیشه حذف میشوند.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'حذف دائمی فایلهای انتخابنشده تورنت پس از تکمیل فعال شود؟ این کار قابل بازگشت نیست.',
|
torrentRemoveUnselectedFileConfirm: 'حذف دائمی فایلهای انتخابنشده تورنت پس از تکمیل فعال شود؟ این کار قابل بازگشت نیست.',
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ const he = {
|
|||||||
torrentPrioritizePiece: 'תעדוף חלקי טורנט',
|
torrentPrioritizePiece: 'תעדוף חלקי טורנט',
|
||||||
torrentPrioritizePieceHint: 'מדיניות תצוגה מקדימה אופציונלית של Aria2: התחלה, סוף או שניהם; לכל אחד אפשר לציין גודל כמו 1M. השינוי חל בהפעלה או בניסיון חוזר.',
|
torrentPrioritizePieceHint: 'מדיניות תצוגה מקדימה אופציונלית של Aria2: התחלה, סוף או שניהם; לכל אחד אפשר לציין גודל כמו 1M. השינוי חל בהפעלה או בניסיון חוזר.',
|
||||||
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
|
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
|
||||||
|
torrentEncryptionPolicy: 'מדיניות הצפנת Torrent',
|
||||||
|
torrentEncryptionPolicyHint: 'מוחלת כשה-Torrent מתחיל או מנסה שוב. בחרו מדיניות אחת כדי לשמור על הגדרות handshake והצפנת payload עקביות.',
|
||||||
|
torrentEncryptionDisabled: 'מושבתת',
|
||||||
|
torrentEncryptionRequireCrypto: 'דרישת handshake מוסווה',
|
||||||
|
torrentEncryptionForceEncryption: 'כפיית payload מוצפן (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'בחרו מדיניות הצפנה תקפה ל-Torrent',
|
||||||
torrentRemoveUnselectedFile: 'מחיקת קבצי Torrent שלא נבחרו לאחר השלמה',
|
torrentRemoveUnselectedFile: 'מחיקת קבצי Torrent שלא נבחרו לאחר השלמה',
|
||||||
torrentRemoveUnselectedFileHint: 'חל רק כאשר נבחרה קבוצת קבצים חלקית. Aria2 מוחק לצמיתות את שאר הקבצים לאחר השלמת ה-Torrent.',
|
torrentRemoveUnselectedFileHint: 'חל רק כאשר נבחרה קבוצת קבצים חלקית. Aria2 מוחק לצמיתות את שאר הקבצים לאחר השלמת ה-Torrent.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'למחוק {{count}} קבצי Torrent שלא נבחרו לאחר השלמה? אי אפשר לבטל פעולה זו.',
|
torrentRemoveUnselectedFileConfirm: 'למחוק {{count}} קבצי Torrent שלא נבחרו לאחר השלמה? אי אפשר לבטל פעולה זו.',
|
||||||
@@ -534,6 +540,12 @@ const he = {
|
|||||||
torrentPrioritizePiece: 'תעדוף חלקי טורנט',
|
torrentPrioritizePiece: 'תעדוף חלקי טורנט',
|
||||||
torrentPrioritizePieceHint: 'נשמר עם הטורנט ומוחל בהפעלה או בניסיון חוזר. יש להזין התחלה, סוף או שניהם עם גודל K או M אופציונלי.',
|
torrentPrioritizePieceHint: 'נשמר עם הטורנט ומוחל בהפעלה או בניסיון חוזר. יש להזין התחלה, סוף או שניהם עם גודל K או M אופציונלי.',
|
||||||
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
|
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
|
||||||
|
torrentEncryptionPolicy: 'מדיניות הצפנת Torrent',
|
||||||
|
torrentEncryptionPolicyHint: 'נשמרת עם ה-Torrent ומוחלת בהפעלה או בניסיון חוזר. המדיניות שומרת על הגדרות ההצפנה של Aria2 עקביות.',
|
||||||
|
torrentEncryptionDisabled: 'מושבתת',
|
||||||
|
torrentEncryptionRequireCrypto: 'דרישת handshake מוסווה',
|
||||||
|
torrentEncryptionForceEncryption: 'כפיית payload מוצפן (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'בחרו מדיניות הצפנה תקפה ל-Torrent',
|
||||||
torrentRemoveUnselectedFile: 'מחיקת קבצי Torrent שלא נבחרו לאחר השלמה',
|
torrentRemoveUnselectedFile: 'מחיקת קבצי Torrent שלא נבחרו לאחר השלמה',
|
||||||
torrentRemoveUnselectedFileHint: 'חל רק כאשר מוגדרת קבוצת קבצים חלקית. הקבצים שלא נבחרו אינם בבעלות Firelink ונמחקים לצמיתות כשה-Torrent מסתיים.',
|
torrentRemoveUnselectedFileHint: 'חל רק כאשר מוגדרת קבוצת קבצים חלקית. הקבצים שלא נבחרו אינם בבעלות Firelink ונמחקים לצמיתות כשה-Torrent מסתיים.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'להפעיל מחיקה לצמיתות של קבצי Torrent שלא נבחרו לאחר השלמה? אי אפשר לבטל פעולה זו.',
|
torrentRemoveUnselectedFileConfirm: 'להפעיל מחיקה לצמיתות של קבצי Torrent שלא נבחרו לאחר השלמה? אי אפשר לבטל פעולה זו.',
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ const ru = {
|
|||||||
torrentPrioritizePiece: 'Приоритет частей торрента',
|
torrentPrioritizePiece: 'Приоритет частей торрента',
|
||||||
torrentPrioritizePieceHint: 'Необязательная политика предпросмотра Aria2: начало, конец или оба варианта; для каждого можно указать размер, например 1M. Применяется при запуске или повторной попытке.',
|
torrentPrioritizePieceHint: 'Необязательная политика предпросмотра Aria2: начало, конец или оба варианта; для каждого можно указать размер, например 1M. Применяется при запуске или повторной попытке.',
|
||||||
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
|
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
|
||||||
|
torrentEncryptionPolicy: 'Политика шифрования Torrent',
|
||||||
|
torrentEncryptionPolicyHint: 'Применяется при запуске или повторной попытке Torrent. Выберите одну политику, чтобы параметры handshake и шифрования payload оставались согласованными.',
|
||||||
|
torrentEncryptionDisabled: 'Отключено',
|
||||||
|
torrentEncryptionRequireCrypto: 'Требовать зашифрованное рукопожатие',
|
||||||
|
torrentEncryptionForceEncryption: 'Принудительно шифровать payload (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'Выберите допустимую политику шифрования Torrent',
|
||||||
torrentRemoveUnselectedFile: 'Удалять невыбранные файлы Torrent после завершения',
|
torrentRemoveUnselectedFile: 'Удалять невыбранные файлы Torrent после завершения',
|
||||||
torrentRemoveUnselectedFileHint: 'Применяется только при выборе части файлов. Aria2 навсегда удалит остальные файлы после завершения Torrent.',
|
torrentRemoveUnselectedFileHint: 'Применяется только при выборе части файлов. Aria2 навсегда удалит остальные файлы после завершения Torrent.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'Удалить {{count}} невыбранных файлов Torrent после завершения? Это действие нельзя отменить.',
|
torrentRemoveUnselectedFileConfirm: 'Удалить {{count}} невыбранных файлов Torrent после завершения? Это действие нельзя отменить.',
|
||||||
@@ -534,6 +540,12 @@ const ru = {
|
|||||||
torrentPrioritizePiece: 'Приоритет частей торрента',
|
torrentPrioritizePiece: 'Приоритет частей торрента',
|
||||||
torrentPrioritizePieceHint: 'Сохраняется с торрентом и применяется при следующем запуске или повторной попытке. Укажите начало, конец или оба варианта с размером K или M.',
|
torrentPrioritizePieceHint: 'Сохраняется с торрентом и применяется при следующем запуске или повторной попытке. Укажите начало, конец или оба варианта с размером K или M.',
|
||||||
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
|
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
|
||||||
|
torrentEncryptionPolicy: 'Политика шифрования Torrent',
|
||||||
|
torrentEncryptionPolicyHint: 'Сохраняется вместе с Torrent и применяется при следующем запуске или повторной попытке. Выбранная политика согласует параметры шифрования Aria2.',
|
||||||
|
torrentEncryptionDisabled: 'Отключено',
|
||||||
|
torrentEncryptionRequireCrypto: 'Требовать зашифрованное рукопожатие',
|
||||||
|
torrentEncryptionForceEncryption: 'Принудительно шифровать payload (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'Выберите допустимую политику шифрования Torrent',
|
||||||
torrentRemoveUnselectedFile: 'Удалять невыбранные файлы Torrent после завершения',
|
torrentRemoveUnselectedFile: 'Удалять невыбранные файлы Torrent после завершения',
|
||||||
torrentRemoveUnselectedFileHint: 'Применяется при настроенном выборе части файлов. Невыбранные файлы не принадлежат Firelink и навсегда удаляются после завершения Torrent.',
|
torrentRemoveUnselectedFileHint: 'Применяется при настроенном выборе части файлов. Невыбранные файлы не принадлежат Firelink и навсегда удаляются после завершения Torrent.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'Включить безвозвратное удаление невыбранных файлов Torrent после завершения? Это действие нельзя отменить.',
|
torrentRemoveUnselectedFileConfirm: 'Включить безвозвратное удаление невыбранных файлов Torrent после завершения? Это действие нельзя отменить.',
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ const uk = {
|
|||||||
torrentPrioritizePiece: 'Пріоритет частин торрента',
|
torrentPrioritizePiece: 'Пріоритет частин торрента',
|
||||||
torrentPrioritizePieceHint: 'Необов’язкова політика попереднього перегляду Aria2: початок, кінець або обидва варіанти; для кожного можна вказати розмір, наприклад 1M. Застосовується під час запуску або повторної спроби.',
|
torrentPrioritizePieceHint: 'Необов’язкова політика попереднього перегляду Aria2: початок, кінець або обидва варіанти; для кожного можна вказати розмір, наприклад 1M. Застосовується під час запуску або повторної спроби.',
|
||||||
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
|
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
|
||||||
|
torrentEncryptionPolicy: 'Політика шифрування Torrent',
|
||||||
|
torrentEncryptionPolicyHint: 'Застосовується під час запуску або повторної спроби Torrent. Виберіть одну політику, щоб параметри handshake і шифрування payload залишалися узгодженими.',
|
||||||
|
torrentEncryptionDisabled: 'Вимкнено',
|
||||||
|
torrentEncryptionRequireCrypto: 'Вимагати зашифроване рукостискання',
|
||||||
|
torrentEncryptionForceEncryption: 'Примусово шифрувати payload (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'Виберіть допустиму політику шифрування Torrent',
|
||||||
torrentRemoveUnselectedFile: 'Видаляти невибрані файли Torrent після завершення',
|
torrentRemoveUnselectedFile: 'Видаляти невибрані файли Torrent після завершення',
|
||||||
torrentRemoveUnselectedFileHint: 'Застосовується лише після вибору частини файлів. Aria2 назавжди видалить решту файлів після завершення Torrent.',
|
torrentRemoveUnselectedFileHint: 'Застосовується лише після вибору частини файлів. Aria2 назавжди видалить решту файлів після завершення Torrent.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'Видалити {{count}} невибраних файлів Torrent після завершення? Цю дію не можна скасувати.',
|
torrentRemoveUnselectedFileConfirm: 'Видалити {{count}} невибраних файлів Torrent після завершення? Цю дію не можна скасувати.',
|
||||||
@@ -534,6 +540,12 @@ const uk = {
|
|||||||
torrentPrioritizePiece: 'Пріоритет частин торрента',
|
torrentPrioritizePiece: 'Пріоритет частин торрента',
|
||||||
torrentPrioritizePieceHint: 'Зберігається разом із торрентом і застосовується під час наступного запуску або повторної спроби. Укажіть початок, кінець або обидва варіанти з розміром K чи M.',
|
torrentPrioritizePieceHint: 'Зберігається разом із торрентом і застосовується під час наступного запуску або повторної спроби. Укажіть початок, кінець або обидва варіанти з розміром K чи M.',
|
||||||
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
|
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
|
||||||
|
torrentEncryptionPolicy: 'Політика шифрування Torrent',
|
||||||
|
torrentEncryptionPolicyHint: 'Зберігається разом із Torrent і застосовується під час наступного запуску або повторної спроби. Вибрана політика узгоджує параметри шифрування Aria2.',
|
||||||
|
torrentEncryptionDisabled: 'Вимкнено',
|
||||||
|
torrentEncryptionRequireCrypto: 'Вимагати зашифроване рукостискання',
|
||||||
|
torrentEncryptionForceEncryption: 'Примусово шифрувати payload (ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: 'Виберіть допустиму політику шифрування Torrent',
|
||||||
torrentRemoveUnselectedFile: 'Видаляти невибрані файли Torrent після завершення',
|
torrentRemoveUnselectedFile: 'Видаляти невибрані файли Torrent після завершення',
|
||||||
torrentRemoveUnselectedFileHint: 'Застосовується для налаштованого вибору частини файлів. Невибрані файли не належать Firelink і назавжди видаляються після завершення Torrent.',
|
torrentRemoveUnselectedFileHint: 'Застосовується для налаштованого вибору частини файлів. Невибрані файли не належать Firelink і назавжди видаляються після завершення Torrent.',
|
||||||
torrentRemoveUnselectedFileConfirm: 'Увімкнути незворотне видалення невибраних файлів Torrent після завершення? Цю дію не можна скасувати.',
|
torrentRemoveUnselectedFileConfirm: 'Увімкнути незворотне видалення невибраних файлів Torrent після завершення? Цю дію не можна скасувати.',
|
||||||
|
|||||||
@@ -268,6 +268,12 @@ const zhCN = {
|
|||||||
torrentPrioritizePiece: '优先下载 Torrent 片段',
|
torrentPrioritizePiece: '优先下载 Torrent 片段',
|
||||||
torrentPrioritizePieceHint: '可选的 Aria2 预览策略:开头、结尾或两者;每项可使用 1M 等大小。Torrent 启动或重试时应用。',
|
torrentPrioritizePieceHint: '可选的 Aria2 预览策略:开头、结尾或两者;每项可使用 1M 等大小。Torrent 启动或重试时应用。',
|
||||||
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
|
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
|
||||||
|
torrentEncryptionPolicy: 'Torrent 加密策略',
|
||||||
|
torrentEncryptionPolicyHint: '在 Torrent 启动或重试时应用。选择单一策略,确保握手和 payload 加密设置保持一致。',
|
||||||
|
torrentEncryptionDisabled: '已禁用',
|
||||||
|
torrentEncryptionRequireCrypto: '要求加密握手',
|
||||||
|
torrentEncryptionForceEncryption: '强制加密 payload(ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: '请选择有效的 Torrent 加密策略',
|
||||||
torrentRemoveUnselectedFile: '完成后删除未选中的 Torrent 文件',
|
torrentRemoveUnselectedFile: '完成后删除未选中的 Torrent 文件',
|
||||||
torrentRemoveUnselectedFileHint: '仅在选择了部分文件时生效。Torrent 完成后,Aria2 会永久删除其余文件。',
|
torrentRemoveUnselectedFileHint: '仅在选择了部分文件时生效。Torrent 完成后,Aria2 会永久删除其余文件。',
|
||||||
torrentRemoveUnselectedFileConfirm: '完成后删除 {{count}} 个未选中的 Torrent 文件?此操作无法撤销。',
|
torrentRemoveUnselectedFileConfirm: '完成后删除 {{count}} 个未选中的 Torrent 文件?此操作无法撤销。',
|
||||||
@@ -534,6 +540,12 @@ const zhCN = {
|
|||||||
torrentPrioritizePiece: '优先下载 Torrent 片段',
|
torrentPrioritizePiece: '优先下载 Torrent 片段',
|
||||||
torrentPrioritizePieceHint: '随 Torrent 保存,并在下次启动或重试时应用。可使用开头、结尾或两者,并可选 K 或 M 大小。',
|
torrentPrioritizePieceHint: '随 Torrent 保存,并在下次启动或重试时应用。可使用开头、结尾或两者,并可选 K 或 M 大小。',
|
||||||
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
|
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
|
||||||
|
torrentEncryptionPolicy: 'Torrent 加密策略',
|
||||||
|
torrentEncryptionPolicyHint: '随 Torrent 保存,并在下次启动或重试时应用。所选策略会保持 Aria2 加密设置一致。',
|
||||||
|
torrentEncryptionDisabled: '已禁用',
|
||||||
|
torrentEncryptionRequireCrypto: '要求加密握手',
|
||||||
|
torrentEncryptionForceEncryption: '强制加密 payload(ARC4)',
|
||||||
|
torrentEncryptionPolicyInvalid: '请选择有效的 Torrent 加密策略',
|
||||||
torrentRemoveUnselectedFile: '完成后删除未选中的 Torrent 文件',
|
torrentRemoveUnselectedFile: '完成后删除未选中的 Torrent 文件',
|
||||||
torrentRemoveUnselectedFileHint: '仅适用于配置了部分文件选择的 Torrent。未选中的文件不属于 Firelink,并会在 Torrent 完成后永久删除。',
|
torrentRemoveUnselectedFileHint: '仅适用于配置了部分文件选择的 Torrent。未选中的文件不属于 Firelink,并会在 Torrent 完成后永久删除。',
|
||||||
torrentRemoveUnselectedFileConfirm: '启用完成后永久删除未选中的 Torrent 文件?此操作无法撤销。',
|
torrentRemoveUnselectedFileConfirm: '启用完成后永久删除未选中的 Torrent 文件?此操作无法撤销。',
|
||||||
|
|||||||
@@ -858,7 +858,8 @@ describe('useDownloadStore', () => {
|
|||||||
torrentExcludeTrackers: 123 as unknown as string,
|
torrentExcludeTrackers: 123 as unknown as string,
|
||||||
torrentStopTimeout: 604801,
|
torrentStopTimeout: 604801,
|
||||||
torrentPrioritizePiece: 'head=1G',
|
torrentPrioritizePiece: 'head=1G',
|
||||||
torrentRemoveUnselectedFile: 'yes' as unknown as boolean
|
torrentRemoveUnselectedFile: 'yes' as unknown as boolean,
|
||||||
|
torrentEncryptionPolicy: 'arc4'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(normalized.torrentMaxPeers).toBeUndefined();
|
expect(normalized.torrentMaxPeers).toBeUndefined();
|
||||||
@@ -869,6 +870,7 @@ describe('useDownloadStore', () => {
|
|||||||
expect(normalized.torrentStopTimeout).toBeUndefined();
|
expect(normalized.torrentStopTimeout).toBeUndefined();
|
||||||
expect(normalized.torrentPrioritizePiece).toBeUndefined();
|
expect(normalized.torrentPrioritizePiece).toBeUndefined();
|
||||||
expect(normalized.torrentRemoveUnselectedFile).toBeUndefined();
|
expect(normalized.torrentRemoveUnselectedFile).toBeUndefined();
|
||||||
|
expect(normalized.torrentEncryptionPolicy).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes proxy settings for download dispatch', async () => {
|
it('normalizes proxy settings for download dispatch', async () => {
|
||||||
@@ -1524,6 +1526,7 @@ describe('useDownloadStore', () => {
|
|||||||
torrentExcludeTrackers: '*',
|
torrentExcludeTrackers: '*',
|
||||||
torrentStopTimeout: 300,
|
torrentStopTimeout: 300,
|
||||||
torrentPrioritizePiece: 'head=1M,tail=1M',
|
torrentPrioritizePiece: 'head=1M,tail=1M',
|
||||||
|
torrentEncryptionPolicy: 'force-encryption',
|
||||||
torrentFileIndices: [1],
|
torrentFileIndices: [1],
|
||||||
torrentRemoveUnselectedFile: true
|
torrentRemoveUnselectedFile: true
|
||||||
}, { type: 'start-now' });
|
}, { type: 'start-now' });
|
||||||
@@ -1541,6 +1544,7 @@ describe('useDownloadStore', () => {
|
|||||||
torrent_exclude_trackers: '*',
|
torrent_exclude_trackers: '*',
|
||||||
torrent_stop_timeout: 300,
|
torrent_stop_timeout: 300,
|
||||||
torrent_prioritize_piece: 'head=1M,tail=1M',
|
torrent_prioritize_piece: 'head=1M,tail=1M',
|
||||||
|
torrent_encryption_policy: 'force-encryption',
|
||||||
torrent_file_indices: [1],
|
torrent_file_indices: [1],
|
||||||
torrent_remove_unselected_file: true
|
torrent_remove_unselected_file: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { ExtensionCookieScope } from '../bindings/ExtensionCookieScope';
|
|||||||
import type { Queue } from '../bindings/Queue';
|
import type { Queue } from '../bindings/Queue';
|
||||||
import { useSettingsStore } from './useSettingsStore';
|
import { useSettingsStore } from './useSettingsStore';
|
||||||
import { useDownloadProgressStore } from './downloadProgressStore';
|
import { useDownloadProgressStore } from './downloadProgressStore';
|
||||||
import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentPrioritizePiece, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
|
import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentPrioritizePiece, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
|
||||||
import {
|
import {
|
||||||
resolveCategoryDestination
|
resolveCategoryDestination
|
||||||
} from '../utils/downloadLocations';
|
} from '../utils/downloadLocations';
|
||||||
@@ -356,6 +356,7 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
|
|||||||
torrent_stop_timeout: item.torrentStopTimeout,
|
torrent_stop_timeout: item.torrentStopTimeout,
|
||||||
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
|
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
|
||||||
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
|
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
|
||||||
|
torrent_encryption_policy: item.torrentEncryptionPolicy || undefined,
|
||||||
lifecycle_generation: lifecycleGeneration.toString(),
|
lifecycle_generation: lifecycleGeneration.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -659,6 +660,8 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
|||||||
const normalizedRemoveUnselectedFile = typeof rawRemoveUnselectedFile === 'boolean'
|
const normalizedRemoveUnselectedFile = typeof rawRemoveUnselectedFile === 'boolean'
|
||||||
? rawRemoveUnselectedFile
|
? rawRemoveUnselectedFile
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const rawEncryptionPolicy = download.torrentEncryptionPolicy as unknown;
|
||||||
|
const normalizedEncryptionPolicy = normalizeTorrentEncryptionPolicy(rawEncryptionPolicy);
|
||||||
const normalizedOptions = rawMaxPeers !== normalizedMaxPeers ||
|
const normalizedOptions = rawMaxPeers !== normalizedMaxPeers ||
|
||||||
rawPeerSpeedLimit !== normalizedPeerSpeedLimit ||
|
rawPeerSpeedLimit !== normalizedPeerSpeedLimit ||
|
||||||
rawCheckIntegrity !== normalizedCheckIntegrity ||
|
rawCheckIntegrity !== normalizedCheckIntegrity ||
|
||||||
@@ -666,7 +669,8 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
|||||||
rawExcludeTrackers !== normalizedExcludeTrackers ||
|
rawExcludeTrackers !== normalizedExcludeTrackers ||
|
||||||
rawStopTimeout !== normalizedStopTimeout ||
|
rawStopTimeout !== normalizedStopTimeout ||
|
||||||
rawPrioritizePiece !== normalizedPrioritizePiece ||
|
rawPrioritizePiece !== normalizedPrioritizePiece ||
|
||||||
rawRemoveUnselectedFile !== normalizedRemoveUnselectedFile
|
rawRemoveUnselectedFile !== normalizedRemoveUnselectedFile ||
|
||||||
|
rawEncryptionPolicy !== normalizedEncryptionPolicy
|
||||||
? {
|
? {
|
||||||
...download,
|
...download,
|
||||||
torrentMaxPeers: normalizedMaxPeers,
|
torrentMaxPeers: normalizedMaxPeers,
|
||||||
@@ -676,7 +680,8 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
|||||||
torrentExcludeTrackers: normalizedExcludeTrackers,
|
torrentExcludeTrackers: normalizedExcludeTrackers,
|
||||||
torrentStopTimeout: normalizedStopTimeout,
|
torrentStopTimeout: normalizedStopTimeout,
|
||||||
torrentPrioritizePiece: normalizedPrioritizePiece,
|
torrentPrioritizePiece: normalizedPrioritizePiece,
|
||||||
torrentRemoveUnselectedFile: normalizedRemoveUnselectedFile
|
torrentRemoveUnselectedFile: normalizedRemoveUnselectedFile,
|
||||||
|
torrentEncryptionPolicy: normalizedEncryptionPolicy
|
||||||
}
|
}
|
||||||
: download;
|
: download;
|
||||||
|
|
||||||
@@ -2213,6 +2218,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
|||||||
torrent_stop_timeout: item.torrentStopTimeout,
|
torrent_stop_timeout: item.torrentStopTimeout,
|
||||||
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
|
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
|
||||||
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
|
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
|
||||||
|
torrent_encryption_policy: item.torrentEncryptionPolicy || undefined,
|
||||||
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
|
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
canonicalizeDownloadFileName,
|
canonicalizeDownloadFileName,
|
||||||
isValidTorrentExcludeTrackerList,
|
isValidTorrentExcludeTrackerList,
|
||||||
isValidTorrentTrackerList,
|
isValidTorrentTrackerList,
|
||||||
|
normalizeTorrentEncryptionPolicy,
|
||||||
normalizeTorrentPrioritizePiece,
|
normalizeTorrentPrioritizePiece,
|
||||||
redactDownloadForPersistence,
|
redactDownloadForPersistence,
|
||||||
resolveDownloadConnections
|
resolveDownloadConnections
|
||||||
@@ -92,6 +93,20 @@ describe('Torrent piece priority validation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Torrent encryption policy validation', () => {
|
||||||
|
it('accepts only the canonical policy states', () => {
|
||||||
|
expect(normalizeTorrentEncryptionPolicy('disabled')).toBe('disabled');
|
||||||
|
expect(normalizeTorrentEncryptionPolicy('require-crypto')).toBe('require-crypto');
|
||||||
|
expect(normalizeTorrentEncryptionPolicy('force-encryption')).toBe('force-encryption');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears unknown or malformed persisted values', () => {
|
||||||
|
expect(normalizeTorrentEncryptionPolicy(undefined)).toBeUndefined();
|
||||||
|
expect(normalizeTorrentEncryptionPolicy('arc4')).toBeUndefined();
|
||||||
|
expect(normalizeTorrentEncryptionPolicy(true)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('download connection resolution', () => {
|
describe('download connection resolution', () => {
|
||||||
it('uses a clamped fallback for legacy rows without a saved value', () => {
|
it('uses a clamped fallback for legacy rows without a saved value', () => {
|
||||||
expect(resolveDownloadConnections(undefined, 8)).toBe(8);
|
expect(resolveDownloadConnections(undefined, 8)).toBe(8);
|
||||||
|
|||||||
@@ -44,6 +44,27 @@ export const isTransferActiveStatus = (status: DownloadStatus): boolean =>
|
|||||||
export const DOWNLOAD_CONNECTIONS_MIN = 1;
|
export const DOWNLOAD_CONNECTIONS_MIN = 1;
|
||||||
export const DOWNLOAD_CONNECTIONS_MAX = 16;
|
export const DOWNLOAD_CONNECTIONS_MAX = 16;
|
||||||
|
|
||||||
|
export const TORRENT_ENCRYPTION_POLICY_DISABLED = 'disabled' as const;
|
||||||
|
export const TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO = 'require-crypto' as const;
|
||||||
|
export const TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION = 'force-encryption' as const;
|
||||||
|
export type TorrentEncryptionPolicy =
|
||||||
|
| typeof TORRENT_ENCRYPTION_POLICY_DISABLED
|
||||||
|
| typeof TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO
|
||||||
|
| typeof TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION;
|
||||||
|
|
||||||
|
export const normalizeTorrentEncryptionPolicy = (
|
||||||
|
value: unknown
|
||||||
|
): TorrentEncryptionPolicy | undefined => {
|
||||||
|
if (
|
||||||
|
value === TORRENT_ENCRYPTION_POLICY_DISABLED ||
|
||||||
|
value === TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO ||
|
||||||
|
value === TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
// Keep every filename component within the common cross-platform filesystem
|
// Keep every filename component within the common cross-platform filesystem
|
||||||
// limit. Count UTF-8 bytes because POSIX filesystems enforce bytes, while this
|
// limit. Count UTF-8 bytes because POSIX filesystems enforce bytes, while this
|
||||||
// bound is also conservative for Windows filename components.
|
// bound is also conservative for Windows filename components.
|
||||||
|
|||||||
Reference in New Issue
Block a user