Skip to content

Commit 98fd433

Browse files
feat: Recursive downloads (part of #49)
chore: Bump deps feat: Ability to set a path for downloads refactor: Use shared HTTP client instance refactor: Use references where applicable feat: Move play command under files
2 parents 7e442bd + 2fba983 commit 98fd433

File tree

8 files changed

+963
-594
lines changed

8 files changed

+963
-594
lines changed

Cargo.lock

Lines changed: 483 additions & 294 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ exclude = [".gitignore", ".github/*"]
1515
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1616

1717
[dependencies]
18-
clap = { version = "4.4.18", features = ["derive"] }
19-
confy = "0.6.0"
20-
serde = { version = "1.0.195", features = ["derive"] }
21-
reqwest = { version = "0.11.23", features = [
18+
clap = { version = "4.5.9", features = ["derive"] }
19+
confy = "0.6.1"
20+
serde = { version = "1.0.204", features = ["derive"] }
21+
reqwest = { version = "0.12.5", features = [
2222
"json",
2323
"blocking",
2424
"multipart",
2525
"native-tls-vendored",
2626
] }
2727
tabled = { version = "0.15.0", features = ["derive"] }
2828
bytefmt = "0.1.7"
29-
serde_json = { version = "1.0.111", features = ["std"] }
30-
serde_with = { version = "3.5.1", features = [] }
29+
serde_json = { version = "1.0.120", features = ["std"] }
30+
serde_with = { version = "3.8.3", features = [] }
3131

3232
[[bin]]
3333
name = "kaput"

src/main.rs

Lines changed: 260 additions & 197 deletions
Large diffs are not rendered by default.

src/put/account.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use reqwest::{blocking::Client, Error};
12
use serde::{Deserialize, Serialize};
23

34
#[derive(Debug, Serialize, Deserialize)]
@@ -13,12 +14,12 @@ pub struct AccountResponse {
1314
}
1415

1516
/// Returns the user's account info.
16-
pub fn info(api_key: String) -> Result<AccountResponse, Box<dyn std::error::Error>> {
17-
let client = reqwest::blocking::Client::new();
17+
pub fn info(client: &Client, api_key: &String) -> Result<AccountResponse, Error> {
1818
let response: AccountResponse = client
1919
.get("https://api.put.io/v2/account/info")
20-
.header("authorization", format!("Bearer {}", api_key))
20+
.header("authorization", format!("Bearer {api_key}"))
2121
.send()?
2222
.json()?;
23+
2324
Ok(response)
2425
}

src/put/files.rs

Lines changed: 143 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
use std::fmt;
1+
use std::process::{Command as ProcessCommand, Stdio};
2+
use std::{fmt, fs};
23

3-
use reqwest::blocking::multipart;
4+
use reqwest::blocking::multipart::Form;
5+
use reqwest::blocking::Client;
6+
use reqwest::Error;
47
use serde::{Deserialize, Serialize};
58
use serde_with::{serde_as, DefaultOnNull};
69
use tabled::Tabled;
710

11+
use crate::put;
12+
813
#[derive(Debug, Serialize, Deserialize)]
914
pub struct FileSize(u64);
1015

@@ -33,18 +38,15 @@ pub struct FilesResponse {
3338
}
3439

3540
/// Returns the user's files.
36-
pub fn list(
37-
api_token: String,
38-
parent_id: u32,
39-
) -> Result<FilesResponse, Box<dyn std::error::Error>> {
40-
let client = reqwest::blocking::Client::new();
41+
pub fn list(client: &Client, api_token: &String, parent_id: u32) -> Result<FilesResponse, Error> {
4142
let response: FilesResponse = client
4243
.get(format!(
4344
"https://api.put.io/v2/files/list?parent_id={parent_id}"
4445
))
45-
.header("authorization", format!("Bearer {}", api_token))
46+
.header("authorization", format!("Bearer {api_token}"))
4647
.send()?
4748
.json()?;
49+
4850
Ok(response)
4951
}
5052

@@ -56,26 +58,27 @@ pub struct SearchResponse {
5658

5759
/// Searches files for given keyword.
5860
pub fn search(
59-
api_token: String,
60-
query: String,
61-
) -> Result<SearchResponse, Box<dyn std::error::Error>> {
62-
let client = reqwest::blocking::Client::new();
61+
client: &Client,
62+
api_token: &String,
63+
query: &String,
64+
) -> Result<SearchResponse, Error> {
6365
let response: SearchResponse = client
6466
.get(format!("https://api.put.io/v2/files/search?query={query}"))
65-
.header("authorization", format!("Bearer {}", api_token))
67+
.header("authorization", format!("Bearer {api_token}"))
6668
.send()?
6769
.json()?;
70+
6871
Ok(response)
6972
}
7073

7174
/// Delete file(s)
72-
pub fn delete(api_token: String, file_id: String) -> Result<(), Box<dyn std::error::Error>> {
73-
let client = reqwest::blocking::Client::new();
74-
let form = multipart::Form::new().text("file_ids", file_id);
75+
pub fn delete(client: &Client, api_token: &String, file_id: &str) -> Result<(), Error> {
76+
let form: Form = Form::new().text("file_ids", file_id.to_owned());
77+
7578
client
7679
.post("https://api.put.io/v2/files/delete")
7780
.multipart(form)
78-
.header("authorization", format!("Bearer {}", api_token))
81+
.header("authorization", format!("Bearer {api_token}"))
7982
.send()?;
8083

8184
Ok(())
@@ -87,70 +90,72 @@ pub struct UrlResponse {
8790
}
8891

8992
/// Returns a download URL for a given file.
90-
pub fn url(api_token: String, file_id: u32) -> Result<UrlResponse, Box<dyn std::error::Error>> {
91-
let client = reqwest::blocking::Client::new();
93+
pub fn url(client: &Client, api_token: &String, file_id: u32) -> Result<UrlResponse, Error> {
9294
let response: UrlResponse = client
9395
.get(format!("https://api.put.io/v2/files/{file_id}/url"))
94-
.header("authorization", format!("Bearer {}", api_token))
96+
.header("authorization", format!("Bearer {api_token}"))
9597
.send()?
9698
.json()?;
99+
97100
Ok(response)
98101
}
99102

100103
/// Moves a file to a different parent
101104
pub fn mv(
102-
api_token: String,
103-
file_id: String,
105+
client: &Client,
106+
api_token: &String,
107+
file_id: u32,
104108
new_parent_id: u32,
105-
) -> Result<(), Box<dyn std::error::Error>> {
106-
let client = reqwest::blocking::Client::new();
107-
let form = multipart::Form::new()
108-
.text("file_ids", file_id)
109+
) -> Result<(), Error> {
110+
let form: Form = Form::new()
111+
.text("file_ids", file_id.to_string())
109112
.text("parent_id", new_parent_id.to_string());
113+
110114
client
111115
.post("https://api.put.io/v2/files/move")
112116
.multipart(form)
113-
.header("authorization", format!("Bearer {}", api_token))
117+
.header("authorization", format!("Bearer {api_token}"))
114118
.send()?;
115119

116120
Ok(())
117121
}
118122

119123
/// Renames a file
120124
pub fn rename(
121-
api_token: String,
125+
client: &Client,
126+
api_token: &String,
122127
file_id: u32,
123-
new_name: String,
124-
) -> Result<(), Box<dyn std::error::Error>> {
125-
let client = reqwest::blocking::Client::new();
126-
let form = multipart::Form::new()
128+
new_name: &String,
129+
) -> Result<(), Error> {
130+
let form = Form::new()
127131
.text("file_id", file_id.to_string())
128-
.text("name", new_name);
132+
.text("name", new_name.to_owned());
133+
129134
client
130135
.post("https://api.put.io/v2/files/rename")
131136
.multipart(form)
132-
.header("authorization", format!("Bearer {}", api_token))
137+
.header("authorization", format!("Bearer {api_token}"))
133138
.send()?;
134139

135140
Ok(())
136141
}
137142

138143
/// Extracts ZIP and RAR archives
139-
pub fn extract(api_token: String, file_id: String) -> Result<(), Box<dyn std::error::Error>> {
140-
let client = reqwest::blocking::Client::new();
141-
let form = multipart::Form::new().text("file_ids", file_id);
144+
pub fn extract(client: &Client, api_token: &String, file_id: u32) -> Result<(), Error> {
145+
let form: Form = Form::new().text("file_ids", file_id.to_string());
146+
142147
client
143148
.post("https://api.put.io/v2/files/extract")
144149
.multipart(form)
145-
.header("authorization", format!("Bearer {}", api_token))
150+
.header("authorization", format!("Bearer {api_token}"))
146151
.send()?;
147152

148153
Ok(())
149154
}
150155

151156
#[derive(Debug, Serialize, Deserialize, Tabled)]
152157
pub struct Extraction {
153-
pub id: u32,
158+
pub id: String,
154159
pub name: String,
155160
pub status: String,
156161
pub message: String,
@@ -162,14 +167,108 @@ pub struct ExtractionResponse {
162167
}
163168

164169
/// Returns active extractions
165-
pub fn get_extractions(
166-
api_token: String,
167-
) -> Result<ExtractionResponse, Box<dyn std::error::Error>> {
168-
let client = reqwest::blocking::Client::new();
170+
pub fn get_extractions(client: &Client, api_token: &String) -> Result<ExtractionResponse, Error> {
169171
let response: ExtractionResponse = client
170172
.get("https://api.put.io/v2/files/extract")
171-
.header("authorization", format!("Bearer {}", api_token))
173+
.header("authorization", format!("Bearer {api_token}"))
172174
.send()?
173175
.json()?;
176+
174177
Ok(response)
175178
}
179+
180+
// Downloads a file or folder
181+
pub fn download(
182+
client: &Client,
183+
api_token: &String,
184+
file_id: u32,
185+
recursive: bool,
186+
path: Option<&String>,
187+
) -> Result<(), Error> {
188+
let files: FilesResponse =
189+
put::files::list(client, api_token, file_id).expect("querying files");
190+
191+
match files.parent.file_type.as_str() {
192+
"FOLDER" => {
193+
// ID is for a folder
194+
match recursive {
195+
true => {
196+
// Recursively download the folder
197+
let directory_path: String = match path {
198+
Some(p) => format!("{}/{}", p, files.parent.name), // Use the provided path if there is one
199+
None => format!("./{}", files.parent.name),
200+
};
201+
202+
fs::create_dir_all(directory_path.clone()).expect("creating directory");
203+
204+
for file in files.files {
205+
download(client, api_token, file.id, true, Some(&directory_path))
206+
.expect("downloading file recursively");
207+
}
208+
}
209+
false => {
210+
// Create a ZIP
211+
println!("Creating ZIP...");
212+
213+
let zip_url: String = put::zips::create(client, api_token, files.parent.id)
214+
.expect("creating zip job");
215+
216+
println!("ZIP created!");
217+
218+
let output_path: String = match path {
219+
Some(p) => format!("{}/{}.zip", p, files.parent.name),
220+
None => format!("./{}.zip", files.parent.name),
221+
};
222+
223+
println!("Downloading: {}", files.parent.name);
224+
println!("Saving to: {}\n", output_path);
225+
226+
// https://rust-lang-nursery.github.io/rust-cookbook/os/external.html#redirect-both-stdout-and-stderr-of-child-process-to-the-same-file
227+
ProcessCommand::new("curl")
228+
.arg("-C")
229+
.arg("-")
230+
.arg("-o")
231+
.arg(output_path)
232+
.arg(zip_url)
233+
.stdout(Stdio::piped())
234+
.spawn()
235+
.expect("failed to run CURL command")
236+
.wait_with_output()
237+
.expect("failed to run CURL command");
238+
239+
println!("\nDownload finished!\n")
240+
}
241+
}
242+
}
243+
_ => {
244+
// ID is for a file
245+
let url_response: UrlResponse =
246+
put::files::url(client, api_token, file_id).expect("creating download URL");
247+
248+
let output_path: String = match path {
249+
Some(p) => format!("{}/{}", p, files.parent.name),
250+
None => format!("./{}", files.parent.name),
251+
};
252+
253+
println!("Downloading: {}", files.parent.name);
254+
println!("Saving to: {}\n", output_path);
255+
256+
// https://rust-lang-nursery.github.io/rust-cookbook/os/external.html#redirect-both-stdout-and-stderr-of-child-process-to-the-same-file
257+
ProcessCommand::new("curl")
258+
.arg("-C")
259+
.arg("-")
260+
.arg("-o")
261+
.arg(output_path)
262+
.arg(url_response.url)
263+
.stdout(Stdio::piped())
264+
.spawn()
265+
.expect("error while spawning curl")
266+
.wait_with_output()
267+
.expect("running CURL command");
268+
269+
println!("\nDownload finished!\n")
270+
}
271+
}
272+
273+
Ok(())
274+
}

src/put/oob.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
use std::collections::HashMap;
22

3+
use reqwest::{blocking::Client, Error};
4+
35
/// Returns a new OOB code.
4-
pub fn get() -> Result<String, Box<dyn std::error::Error>> {
5-
let resp = reqwest::blocking::get("https://api.put.io/v2/oauth2/oob/code?app_id=4701")?
6+
pub fn get(client: &Client) -> Result<String, Error> {
7+
let resp = client
8+
.get("https://api.put.io/v2/oauth2/oob/code?app_id=4701")
9+
.send()?
610
.json::<HashMap<String, String>>()?;
7-
let code = resp.get("code").expect("fetching OOB code");
8-
Ok(code.to_string())
11+
12+
let code: &String = resp.get("code").expect("fetching OOB code");
13+
14+
Ok(code.clone())
915
}
1016

1117
/// Returns new OAuth token if the OOB code is linked to the user's account.
12-
pub fn check(oob_code: String) -> Result<String, Box<dyn std::error::Error>> {
13-
let resp = reqwest::blocking::get(format!(
14-
"https://api.put.io/v2/oauth2/oob/code/{}",
15-
oob_code
16-
))?
17-
.json::<HashMap<String, String>>()?;
18-
let token = resp.get("oauth_token").expect("deserializing OAuth token");
19-
Ok(token.to_string())
18+
pub fn check(client: &Client, oob_code: &String) -> Result<String, Error> {
19+
let resp = client
20+
.get(format!("https://api.put.io/v2/oauth2/oob/code/{oob_code}"))
21+
.send()?
22+
.json::<HashMap<String, String>>()?;
23+
24+
let token: &String = resp.get("oauth_token").expect("fetching OAuth token");
25+
26+
Ok(token.clone())
2027
}

0 commit comments

Comments
 (0)