Import pipeline: probe, hardlink, rename, layout (#82)
ci / rust (push) Failing after 1m43s
ci / web (push) Successful in 58s
e2e / e2e (push) Successful in 1m25s

This commit was merged in pull request #82.
This commit is contained in:
2026-08-22 23:21:30 +01:00
parent 62aba6315c
commit ffb8485939
18 changed files with 1653 additions and 20 deletions
+134
View File
@@ -70,6 +70,24 @@ pub struct Torrent {
pub labels: Vec<String>,
}
/// One file inside a torrent, as Transmission reports it.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TorrentFile {
/// Path relative to the torrent's download directory, torrent folder
/// included.
pub path: PathBuf,
/// Size in bytes.
pub size: u64,
}
/// Where one torrent's data lives on disk.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TorrentContent {
pub hash: String,
pub download_dir: PathBuf,
pub files: Vec<TorrentFile>,
}
/// A Transmission RPC failure.
#[derive(Debug, thiserror::Error)]
pub enum Error {
@@ -218,6 +236,46 @@ impl TransmissionClient {
Ok(response.torrents.into_iter().map(Torrent::from).collect())
}
/// The file list and download directory of one torrent, by infohash.
///
/// `None` when Transmission no longer knows the hash — the operator may
/// have removed the torrent by hand.
///
/// # Errors
///
/// Returns an error for transport failures or rejected/malformed RPC
/// responses.
pub async fn torrent_content(&self, hash: &str) -> Result<Option<TorrentContent>, Error> {
let arguments = self
.call(
"torrent-get",
json!({
"ids": [hash],
"fields": ["hashString", "downloadDir", "files"]
}),
)
.await?;
let response: RpcContentList = serde_json::from_value(arguments)
.map_err(|error| Error::InvalidResponse(error.to_string()))?;
Ok(response
.torrents
.into_iter()
.next()
.map(|torrent| TorrentContent {
hash: torrent.hash,
download_dir: torrent.download_dir,
files: torrent
.files
.into_iter()
.map(|file| TorrentFile {
path: file.name,
size: file.length,
})
.collect(),
}))
}
/// Remove one torrent, optionally deleting its downloaded data.
///
/// # Errors
@@ -291,6 +349,27 @@ struct RpcTorrentList {
torrents: Vec<RpcTorrent>,
}
#[derive(Debug, Deserialize)]
struct RpcContentList {
torrents: Vec<RpcContent>,
}
#[derive(Debug, Deserialize)]
struct RpcContent {
#[serde(rename = "hashString")]
hash: String,
#[serde(rename = "downloadDir")]
download_dir: PathBuf,
#[serde(default)]
files: Vec<RpcFile>,
}
#[derive(Debug, Deserialize)]
struct RpcFile {
name: PathBuf,
length: u64,
}
#[derive(Debug, Deserialize)]
struct RpcTorrent {
id: i64,
@@ -459,6 +538,61 @@ mod tests {
assert_eq!(torrents[0].state, TorrentState::Unknown(99));
}
#[tokio::test]
async fn torrent_content_lists_files_and_download_dir() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(body_partial_json(json!({
"method": "torrent-get",
"arguments": {"ids": ["abc"]}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"result": "success",
"arguments": {"torrents": [{
"hashString": "abc",
"downloadDir": "/downloads",
"files": [
{"name": "Movie/Movie.mkv", "length": 100, "bytesCompleted": 100},
{"name": "Movie/Movie.nfo", "length": 5, "bytesCompleted": 5}
]
}]}
})))
.expect(1)
.mount(&server)
.await;
let client = TransmissionClient::new(&server.uri()).expect("client");
let content = client
.torrent_content("abc")
.await
.expect("content")
.expect("torrent known");
assert_eq!(content.download_dir, PathBuf::from("/downloads"));
assert_eq!(content.files.len(), 2);
assert_eq!(content.files[0].path, PathBuf::from("Movie/Movie.mkv"));
assert_eq!(content.files[0].size, 100);
}
#[tokio::test]
async fn a_torrent_transmission_forgot_is_none_not_an_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"result": "success",
"arguments": {"torrents": []}
})))
.mount(&server)
.await;
let client = TransmissionClient::new(&server.uri()).expect("client");
assert!(client
.torrent_content("gone")
.await
.expect("call succeeds")
.is_none());
}
fn success() -> ResponseTemplate {
ResponseTemplate::new(200).set_body_json(json!({"result": "success", "arguments": {}}))
}