1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#![allow(private_interfaces, private_bounds)]
use std::{fmt::Debug, sync::Mutex};
use tap::Pipe;
use url::Url;
use crate::{Client, LoginState, Qbit, ext::Cookie, model::{ApiKey, Credential}};
pub struct QbitBuilder<C = (), R = (), E = ()> {
credential: C,
client: R,
endpoint: E,
}
trait IntoLoginState {
fn into_login_state(self) -> LoginState;
}
impl IntoLoginState for Cookie {
fn into_login_state(self) -> LoginState {
LoginState::CookieProvided { cookie: self.0 }
}
}
impl IntoLoginState for ApiKey {
fn into_login_state(self) -> LoginState {
LoginState::ApiKeyProvided { api_key: format!("Bearer {}", self.0) }
}
}
impl IntoLoginState for Credential {
fn into_login_state(self) -> LoginState {
LoginState::NotLoggedIn { credential: self }
}
}
impl QbitBuilder {
/// Creates a new `QbitBuilder` with default values.
pub fn new() -> Self {
QbitBuilder {
credential: (),
client: (),
endpoint: (),
}
}
}
impl Default for QbitBuilder {
fn default() -> Self {
Self::new()
}
}
impl<C, R, E> QbitBuilder<C, R, E> {
/// Sets the HTTP client for the `Qbit` instance.
///
/// - When `reqwest` feature is enabled (by default), this method accepts a
/// `reqwest::Client`.
/// - When `cyper` feature is enabled, this method accepts a
/// `cyper::Client`.
pub fn client(self, client: Client) -> QbitBuilder<C, Client, E> {
QbitBuilder {
credential: self.credential,
client,
endpoint: self.endpoint,
}
}
/// Sets the cookie for authentication.
///
/// Note that if you have already set the credential, this method will
/// overwrite the credential and use the cookie instead. The builder
/// will use the latest provided credential for authentication.
#[allow(private_interfaces)]
pub fn cookie(self, cookie: impl Into<String>) -> QbitBuilder<Cookie, R, E> {
QbitBuilder {
credential: Cookie(cookie.into()),
client: self.client,
endpoint: self.endpoint,
}
}
/// Sets the api key for authentication.
///
/// Note that if you have already set the credential, this method will
/// overwrite the credential and use the api key instead. The builder
/// will use the latest provided credential for authentication.
#[allow(private_interfaces)]
pub fn api_key(self, api_key: impl Into<String>) -> QbitBuilder<ApiKey, R, E> {
QbitBuilder {
credential: ApiKey(api_key.into()),
client: self.client,
endpoint: self.endpoint,
}
}
/// Sets the username-password credentials for authentication.
///
/// Note that if you have already set the cookie, this method will overwrite
/// the cookie and use the credential instead. The builder will use the
/// latest provided credential for authentication.
pub fn credential(self, credential: Credential) -> QbitBuilder<Credential, R, E> {
QbitBuilder {
credential,
client: self.client,
endpoint: self.endpoint,
}
}
/// Sets the endpoint URL for the qBittorrent Web API.
pub fn endpoint<U>(self, endpoint: U) -> QbitBuilder<C, R, U>
where
U: TryInto<Url>,
{
QbitBuilder {
credential: self.credential,
client: self.client,
endpoint,
}
}
}
impl<C, U> QbitBuilder<C, Client, U>
where
C: IntoLoginState,
U: TryInto<Url>,
U::Error: Debug,
{
/// Builds the `Qbit` instance with the provided configuration and HTTP
/// Client.
pub fn build(self) -> Qbit {
let mut endpoint: Url = self.endpoint.try_into().expect("Invalid endpoint");
// The API path is resolved against the endpoint with `Url::join`, which
// treats the last path segment as a file and replaces it unless the
// endpoint ends with a slash. Append one so a path prefix such as
// `https://host/prefix` is preserved instead of being clobbered.
// See <https://github.com/George-Miao/qbit/issues/41>.
if !endpoint.path().ends_with('/') {
let path = format!("{}/", endpoint.path());
endpoint.set_path(&path);
}
let state = self.credential.into_login_state().pipe(Mutex::new);
Qbit {
client: self.client,
endpoint,
state,
}
}
}
impl<C, U> QbitBuilder<C, (), U>
where
C: IntoLoginState,
U: TryInto<Url>,
U::Error: Debug,
{
/// Builds the `Qbit` instance with the provided configuration and a default
/// HTTP Client.
pub fn build(self) -> Qbit {
self.client(Client::new()).build()
}
}
#[test]
fn test_builder() {
QbitBuilder::new()
.client(Client::new())
.endpoint("http://localhost:8080")
.credential(Credential::new("admin", "adminadmin"))
.build();
QbitBuilder::new()
.endpoint("http://localhost:8080")
.credential(Credential::new("admin", "adminadmin"))
.build();
QbitBuilder::new()
.client(Client::new())
.endpoint("http://localhost:8080")
.cookie("SID=1234567890")
.build();
QbitBuilder::new()
.endpoint("http://localhost:8080")
.cookie("SID=1234567890")
.build();
QbitBuilder::new()
.endpoint("http://localhost:8080")
.api_key("1234567890")
.build();
}
#[test]
fn test_endpoint_trailing_slash() {
// A path prefix without a trailing slash must be preserved, not clobbered
// by the API path. See https://github.com/George-Miao/qbit/issues/41.
let qbit = QbitBuilder::new()
.endpoint("https://qbit.example/path/prefix")
.credential(Credential::new("admin", "adminadmin"))
.build();
assert_eq!(
qbit.url("auth/login").as_str(),
"https://qbit.example/path/prefix/api/v2/auth/login"
);
// An endpoint that already ends with a slash is left untouched.
let qbit = QbitBuilder::new()
.endpoint("https://qbit.example/path/prefix/")
.credential(Credential::new("admin", "adminadmin"))
.build();
assert_eq!(
qbit.url("auth/login").as_str(),
"https://qbit.example/path/prefix/api/v2/auth/login"
);
}