commit
11a333bef4
|
@ -0,0 +1,2 @@
|
||||||
|
target/
|
||||||
|
result*
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
||||||
|
[workspace]
|
||||||
|
members = [ "crates/*" ]
|
|
@ -0,0 +1,12 @@
|
||||||
|
YOLO LICENSE
|
||||||
|
Version 1, July 10 2015
|
||||||
|
|
||||||
|
THIS SOFTWARE LICENSE IS PROVIDED "ALL CAPS" SO THAT YOU KNOW IT IS SUPER
|
||||||
|
SERIOUS AND YOU DON'T MESS AROUND WITH COPYRIGHT LAW BECAUSE YOU WILL GET IN
|
||||||
|
TROUBLE HERE ARE SOME OTHER BUZZWORDS COMMONLY IN THESE THINGS WARRANTIES
|
||||||
|
LIABILITY CONTRACT TORT LIABLE CLAIMS RESTRICTION MERCHANTABILITY SUBJECT TO
|
||||||
|
THE FOLLOWING CONDITIONS:
|
||||||
|
|
||||||
|
1. #yolo
|
||||||
|
2. #swag
|
||||||
|
3. #blazeit
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "lena"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
.idea
|
|
@ -0,0 +1,27 @@
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- deploy-dryrun
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
Build and test Rust code:
|
||||||
|
stage: test
|
||||||
|
image: rustlang/rust:nightly
|
||||||
|
script:
|
||||||
|
- cargo test
|
||||||
|
|
||||||
|
Test publishing crate:
|
||||||
|
stage: deploy-dryrun
|
||||||
|
image: rustlang/rust:nightly
|
||||||
|
script:
|
||||||
|
- cargo publish --dry-run
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
||||||
|
Publish crate:
|
||||||
|
stage: deploy
|
||||||
|
image: rustlang/rust:nightly
|
||||||
|
script:
|
||||||
|
- sh -c 'cargo login "${CARGO_TOKEN}" && cargo publish'
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "transmission_rs"
|
||||||
|
version = "0.5.0"
|
||||||
|
authors = ["Mike Cronce <mike@cronce.io>"]
|
||||||
|
edition = "2018"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://gitlab.cronce.io/foss/transmission-rs"
|
||||||
|
categories = ["api-bindings"]
|
||||||
|
keywords = ["torrent", "bittorrent", "transmission"]
|
||||||
|
description = "A safe, ergonomic, async client for the Transmission BitTorrent client implemented in pure Rust"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = {version = "0.11", features = ["gzip", "json"]}
|
||||||
|
serde = {version = "1", features = ["derive"]}
|
||||||
|
serde_json = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
tokio = "1"
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Steven vanZyl
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,5 @@
|
||||||
|
# transmission-rs
|
||||||
|
Ergonomic Rust bindings for the [Transmission](https://transmissionbt.com/) BitTorrent client implemented in pure Rust.
|
||||||
|
|
||||||
|
Documentation coming Soon(TM)
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,179 @@
|
||||||
|
//! Client for download management.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
mod payload;
|
||||||
|
use payload::Request;
|
||||||
|
use payload::Response;
|
||||||
|
mod list;
|
||||||
|
use list::FieldList;
|
||||||
|
use list::TorrentList;
|
||||||
|
pub mod torrent;
|
||||||
|
use torrent::Torrent;
|
||||||
|
use torrent::TorrentFile;
|
||||||
|
use torrent::AddTorrent;
|
||||||
|
use torrent::AddTorrentResponse;
|
||||||
|
use torrent::GetTorrentFilesRequest;
|
||||||
|
use torrent::GetTorrentFilesResponse;
|
||||||
|
//use torrent::AddedTorrent;
|
||||||
|
|
||||||
|
/// Interface into the major functions of Transmission
|
||||||
|
/// including adding, and removing torrents.
|
||||||
|
///
|
||||||
|
/// The `Client` does not keep track of the created torrents itself.
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Client {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
tls: bool,
|
||||||
|
auth: Option<(String, String)>,
|
||||||
|
|
||||||
|
base_url: String,
|
||||||
|
session_id: Option<String>,
|
||||||
|
|
||||||
|
http: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
// TODO: Take Option<(&str, &str)> for auth
|
||||||
|
pub fn new(host: impl ToString, port: u16, tls: bool, auth: Option<(String, String)>) -> Result<Self, Error> {
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(reqwest::header::USER_AGENT, "transmission-rs/0.1".try_into()?);
|
||||||
|
let this = Self{
|
||||||
|
host: host.to_string(),
|
||||||
|
port: port,
|
||||||
|
tls: tls,
|
||||||
|
auth: auth,
|
||||||
|
base_url: {
|
||||||
|
let protocol = match tls {
|
||||||
|
true => "https",
|
||||||
|
false => "http"
|
||||||
|
};
|
||||||
|
format!("{}://{}:{}", protocol, host.to_string(), port)
|
||||||
|
},
|
||||||
|
session_id: None,
|
||||||
|
http: reqwest::Client::builder()
|
||||||
|
.gzip(true)
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()?
|
||||||
|
};
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_url(&self, path: impl AsRef<str>) -> String {
|
||||||
|
format!("{}{}", self.base_url, path.as_ref()).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate(&mut self) -> Result<(), Error> {
|
||||||
|
self.session_id = None;
|
||||||
|
let response = self.post("/transmission/rpc/")
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
self.session_id = match response.headers().get("X-Transmission-Session-Id") {
|
||||||
|
Some(v) => Some(v.to_str()?.to_string()),
|
||||||
|
None => None // TODO: Return an error
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post(&self, path: impl AsRef<str>) -> reqwest::RequestBuilder {
|
||||||
|
let url = self.build_url(path);
|
||||||
|
//println!("{}", url);
|
||||||
|
let mut request = self.http.post(&url);
|
||||||
|
request = match &self.auth {
|
||||||
|
Some(auth) => request.basic_auth(&auth.0, Some(&auth.1)),
|
||||||
|
None => request
|
||||||
|
};
|
||||||
|
request = match &self.session_id {
|
||||||
|
Some(token) => request.header("X-Transmission-Session-Id", token),
|
||||||
|
None => request
|
||||||
|
};
|
||||||
|
request
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rpc(&mut self, method: impl ToString, tag: u8, body: impl Serialize) -> Result<reqwest::Response, Error> {
|
||||||
|
let request = Request::new(method, tag, body);
|
||||||
|
let mut error = None;
|
||||||
|
for i in 0..5 {
|
||||||
|
sleep(Duration::from_secs(i)).await;
|
||||||
|
let result = self.post("/transmission/rpc/")
|
||||||
|
// Content-Type doesn't actually appear to be necessary, and is
|
||||||
|
// technically a lie, since there's no key/value being passed,
|
||||||
|
// just some JSON, but the official client sends this, so we
|
||||||
|
// will too.
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body(serde_json::to_string(&request)?)
|
||||||
|
.send().await?
|
||||||
|
.error_for_status();
|
||||||
|
match result {
|
||||||
|
Ok(r) => return Ok(r),
|
||||||
|
Err(e) => match e.status() {
|
||||||
|
Some(reqwest::StatusCode::CONFLICT) => {
|
||||||
|
self.authenticate().await?;
|
||||||
|
error = Some(e);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
_ => return Err(e.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match error {
|
||||||
|
Some(e) => Err(e.into()),
|
||||||
|
// Should be unreachable
|
||||||
|
None => Err(Error::HTTPUnknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(&mut self) -> Result<Vec<Torrent>, Error> {
|
||||||
|
let field_list = FieldList::from_vec(vec![
|
||||||
|
"error",
|
||||||
|
"errorString",
|
||||||
|
"eta",
|
||||||
|
"id",
|
||||||
|
"isFinished",
|
||||||
|
"leftUntilDone",
|
||||||
|
"name",
|
||||||
|
"peersGettingFromUs",
|
||||||
|
"peersSendingToUs",
|
||||||
|
"rateDownload",
|
||||||
|
"rateUpload",
|
||||||
|
"sizeWhenDone",
|
||||||
|
"status",
|
||||||
|
"uploadRatio"
|
||||||
|
]);
|
||||||
|
let response: Response<TorrentList> = self.rpc("torrent-get", 4, field_list).await?.error_for_status()?.json().await?;
|
||||||
|
Ok(response.arguments.torrents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Borrow key from value
|
||||||
|
pub async fn list_by_name(&mut self) -> Result<BTreeMap<String, Torrent>, Error> {
|
||||||
|
Ok(self.list().await?.into_iter().map(|v| (v.name.clone(), v)).collect::<BTreeMap<_, _>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_torrent_from_link(&mut self, url: impl ToString) -> Result<AddTorrentResponse, Error> {
|
||||||
|
let response: Response<AddTorrentResponse> = self.rpc("torrent-add", 8, AddTorrent{filename: url.to_string()}).await?.error_for_status()?.json().await?;
|
||||||
|
Ok(response.arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_files_by_id(&mut self, id: u16) -> Result<Vec<TorrentFile>, Error> {
|
||||||
|
let response: Response<GetTorrentFilesResponse> = self.rpc("torrent-get", 3, GetTorrentFilesRequest::new(id)).await?.error_for_status()?.json().await?;
|
||||||
|
if(response.arguments.torrents.len() == 0) {
|
||||||
|
// TODO: Maybe make the Ok result an Option? Or maybe return a 404 error?
|
||||||
|
return Ok(vec![]);
|
||||||
|
} else if(response.arguments.torrents.len() > 1) {
|
||||||
|
return Err(Error::WeirdGetFilesResponse);
|
||||||
|
}
|
||||||
|
// TODO: Figure out how to move files out of this structure, since we're discarding the rest anyway, instead of cloning
|
||||||
|
Ok(response.arguments.torrents[0].files.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::torrent::Torrent;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct TorrentList {
|
||||||
|
pub torrents: Vec<Torrent>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct FieldList {
|
||||||
|
pub fields: Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldList {
|
||||||
|
pub fn from_vec(vec: Vec<impl ToString>) -> Self {
|
||||||
|
Self{
|
||||||
|
fields: vec.iter().map(|s| s.to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Request<T> {
|
||||||
|
method: String,
|
||||||
|
tag: u8,
|
||||||
|
pub arguments: T
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Request<T> {
|
||||||
|
pub fn new(method: impl ToString, tag: u8, arguments: T) -> Self {
|
||||||
|
Self{
|
||||||
|
method: method.to_string(),
|
||||||
|
tag: tag,
|
||||||
|
arguments: arguments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Response<T> {
|
||||||
|
result: String,
|
||||||
|
tag: u8,
|
||||||
|
pub arguments: T
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Torrent {
|
||||||
|
pub id: u16,
|
||||||
|
pub error: u8,
|
||||||
|
pub error_string: String,
|
||||||
|
pub eta: i64, // TODO: Option<u64> with -1 as None
|
||||||
|
pub is_finished: bool,
|
||||||
|
pub left_until_done: u64, // TODO: u32?
|
||||||
|
pub name: String,
|
||||||
|
pub peers_getting_from_us: u16,
|
||||||
|
pub peers_sending_to_us: u16,
|
||||||
|
pub rate_download: u32,
|
||||||
|
pub rate_upload: u32,
|
||||||
|
pub size_when_done: u64,
|
||||||
|
pub status: u8,
|
||||||
|
pub upload_ratio: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct AddTorrent {
|
||||||
|
pub filename: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AddedTorrent {
|
||||||
|
pub id: u16,
|
||||||
|
pub name: String,
|
||||||
|
pub hash_string: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum AddTorrentResponse {
|
||||||
|
TorrentAdded(AddedTorrent),
|
||||||
|
TorrentDuplicate(AddedTorrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct GetTorrentFilesRequest {
|
||||||
|
fields: Vec<String>,
|
||||||
|
ids: u16
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetTorrentFilesRequest {
|
||||||
|
pub fn new(id: u16) -> Self {
|
||||||
|
Self{
|
||||||
|
fields: vec!["files".to_string()],
|
||||||
|
ids: id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TorrentFile {
|
||||||
|
pub name: PathBuf,
|
||||||
|
pub bytes_completed: u64,
|
||||||
|
pub length: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TorrentFile {
|
||||||
|
pub fn take_name(self) -> PathBuf {
|
||||||
|
self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TorrentFileWrapper {
|
||||||
|
pub files: Vec<TorrentFile>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct GetTorrentFilesResponse {
|
||||||
|
pub torrents: Vec<TorrentFileWrapper>
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("HTTP error")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
#[error("HTTP header decoding error")]
|
||||||
|
ReqwestHeaderDecode(#[from] reqwest::header::ToStrError),
|
||||||
|
#[error("invalid HTTP header value")]
|
||||||
|
ReqwestHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
|
||||||
|
#[error("Unknown HTTP failure")]
|
||||||
|
HTTPUnknown,
|
||||||
|
#[error("JSON error")]
|
||||||
|
JSON(#[from] serde_json::Error),
|
||||||
|
#[error("got more than one 'torrents' entry in a get files response")]
|
||||||
|
WeirdGetFilesResponse,
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
#![allow(unused_parens)]
|
||||||
|
//! Ergonomic Rust bindings for the [Transmission](https://transmissionbt.com/) BitTorrent client
|
||||||
|
//! based on [transmission-sys](https://gitlab.com/tornado-torrent/transmission-sys).
|
||||||
|
//!
|
||||||
|
//! Most interaction will be done through the `Client` struct.
|
||||||
|
|
||||||
|
extern crate serde;
|
||||||
|
extern crate serde_json;
|
||||||
|
|
||||||
|
// Re-exports
|
||||||
|
mod client;
|
||||||
|
pub use client::Client;
|
||||||
|
pub use client::torrent::Torrent;
|
||||||
|
pub use client::torrent::AddTorrentResponse;
|
||||||
|
pub use client::torrent::TorrentFile;
|
||||||
|
mod error;
|
||||||
|
pub use error::Error;
|
||||||
|
|
Loading…
Reference in New Issue