Skip to content

Commit

Permalink
feat: Recursive downloads (part of #49)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
davidchalifoux authored Jul 12, 2024
2 parents 7e442bd + 2fba983 commit 98fd433
Show file tree
Hide file tree
Showing 8 changed files with 963 additions and 594 deletions.
777 changes: 483 additions & 294 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ exclude = [".gitignore", ".github/*"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.4.18", features = ["derive"] }
confy = "0.6.0"
serde = { version = "1.0.195", features = ["derive"] }
reqwest = { version = "0.11.23", features = [
clap = { version = "4.5.9", features = ["derive"] }
confy = "0.6.1"
serde = { version = "1.0.204", features = ["derive"] }
reqwest = { version = "0.12.5", features = [
"json",
"blocking",
"multipart",
"native-tls-vendored",
] }
tabled = { version = "0.15.0", features = ["derive"] }
bytefmt = "0.1.7"
serde_json = { version = "1.0.111", features = ["std"] }
serde_with = { version = "3.5.1", features = [] }
serde_json = { version = "1.0.120", features = ["std"] }
serde_with = { version = "3.8.3", features = [] }

[[bin]]
name = "kaput"
Expand Down
457 changes: 260 additions & 197 deletions src/main.rs

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions src/put/account.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use reqwest::{blocking::Client, Error};
use serde::{Deserialize, Serialize};

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

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

Ok(response)
}
187 changes: 143 additions & 44 deletions src/put/files.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use std::fmt;
use std::process::{Command as ProcessCommand, Stdio};
use std::{fmt, fs};

use reqwest::blocking::multipart;
use reqwest::blocking::multipart::Form;
use reqwest::blocking::Client;
use reqwest::Error;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DefaultOnNull};
use tabled::Tabled;

use crate::put;

#[derive(Debug, Serialize, Deserialize)]
pub struct FileSize(u64);

Expand Down Expand Up @@ -33,18 +38,15 @@ pub struct FilesResponse {
}

/// Returns the user's files.
pub fn list(
api_token: String,
parent_id: u32,
) -> Result<FilesResponse, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
pub fn list(client: &Client, api_token: &String, parent_id: u32) -> Result<FilesResponse, Error> {
let response: FilesResponse = client
.get(format!(
"https://api.put.io/v2/files/list?parent_id={parent_id}"
))
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?
.json()?;

Ok(response)
}

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

/// Searches files for given keyword.
pub fn search(
api_token: String,
query: String,
) -> Result<SearchResponse, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
client: &Client,
api_token: &String,
query: &String,
) -> Result<SearchResponse, Error> {
let response: SearchResponse = client
.get(format!("https://api.put.io/v2/files/search?query={query}"))
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?
.json()?;

Ok(response)
}

/// Delete file(s)
pub fn delete(api_token: String, file_id: String) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
let form = multipart::Form::new().text("file_ids", file_id);
pub fn delete(client: &Client, api_token: &String, file_id: &str) -> Result<(), Error> {
let form: Form = Form::new().text("file_ids", file_id.to_owned());

client
.post("https://api.put.io/v2/files/delete")
.multipart(form)
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?;

Ok(())
Expand All @@ -87,70 +90,72 @@ pub struct UrlResponse {
}

/// Returns a download URL for a given file.
pub fn url(api_token: String, file_id: u32) -> Result<UrlResponse, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
pub fn url(client: &Client, api_token: &String, file_id: u32) -> Result<UrlResponse, Error> {
let response: UrlResponse = client
.get(format!("https://api.put.io/v2/files/{file_id}/url"))
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?
.json()?;

Ok(response)
}

/// Moves a file to a different parent
pub fn mv(
api_token: String,
file_id: String,
client: &Client,
api_token: &String,
file_id: u32,
new_parent_id: u32,
) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
let form = multipart::Form::new()
.text("file_ids", file_id)
) -> Result<(), Error> {
let form: Form = Form::new()
.text("file_ids", file_id.to_string())
.text("parent_id", new_parent_id.to_string());

client
.post("https://api.put.io/v2/files/move")
.multipart(form)
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?;

Ok(())
}

/// Renames a file
pub fn rename(
api_token: String,
client: &Client,
api_token: &String,
file_id: u32,
new_name: String,
) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
let form = multipart::Form::new()
new_name: &String,
) -> Result<(), Error> {
let form = Form::new()
.text("file_id", file_id.to_string())
.text("name", new_name);
.text("name", new_name.to_owned());

client
.post("https://api.put.io/v2/files/rename")
.multipart(form)
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?;

Ok(())
}

/// Extracts ZIP and RAR archives
pub fn extract(api_token: String, file_id: String) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
let form = multipart::Form::new().text("file_ids", file_id);
pub fn extract(client: &Client, api_token: &String, file_id: u32) -> Result<(), Error> {
let form: Form = Form::new().text("file_ids", file_id.to_string());

client
.post("https://api.put.io/v2/files/extract")
.multipart(form)
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?;

Ok(())
}

#[derive(Debug, Serialize, Deserialize, Tabled)]
pub struct Extraction {
pub id: u32,
pub id: String,
pub name: String,
pub status: String,
pub message: String,
Expand All @@ -162,14 +167,108 @@ pub struct ExtractionResponse {
}

/// Returns active extractions
pub fn get_extractions(
api_token: String,
) -> Result<ExtractionResponse, Box<dyn std::error::Error>> {
let client = reqwest::blocking::Client::new();
pub fn get_extractions(client: &Client, api_token: &String) -> Result<ExtractionResponse, Error> {
let response: ExtractionResponse = client
.get("https://api.put.io/v2/files/extract")
.header("authorization", format!("Bearer {}", api_token))
.header("authorization", format!("Bearer {api_token}"))
.send()?
.json()?;

Ok(response)
}

// Downloads a file or folder
pub fn download(
client: &Client,
api_token: &String,
file_id: u32,
recursive: bool,
path: Option<&String>,
) -> Result<(), Error> {
let files: FilesResponse =
put::files::list(client, api_token, file_id).expect("querying files");

match files.parent.file_type.as_str() {
"FOLDER" => {
// ID is for a folder
match recursive {
true => {
// Recursively download the folder
let directory_path: String = match path {
Some(p) => format!("{}/{}", p, files.parent.name), // Use the provided path if there is one
None => format!("./{}", files.parent.name),
};

fs::create_dir_all(directory_path.clone()).expect("creating directory");

for file in files.files {
download(client, api_token, file.id, true, Some(&directory_path))
.expect("downloading file recursively");
}
}
false => {
// Create a ZIP
println!("Creating ZIP...");

let zip_url: String = put::zips::create(client, api_token, files.parent.id)
.expect("creating zip job");

println!("ZIP created!");

let output_path: String = match path {
Some(p) => format!("{}/{}.zip", p, files.parent.name),
None => format!("./{}.zip", files.parent.name),
};

println!("Downloading: {}", files.parent.name);
println!("Saving to: {}\n", output_path);

// https://rust-lang-nursery.github.io/rust-cookbook/os/external.html#redirect-both-stdout-and-stderr-of-child-process-to-the-same-file
ProcessCommand::new("curl")
.arg("-C")
.arg("-")
.arg("-o")
.arg(output_path)
.arg(zip_url)
.stdout(Stdio::piped())
.spawn()
.expect("failed to run CURL command")
.wait_with_output()
.expect("failed to run CURL command");

println!("\nDownload finished!\n")
}
}
}
_ => {
// ID is for a file
let url_response: UrlResponse =
put::files::url(client, api_token, file_id).expect("creating download URL");

let output_path: String = match path {
Some(p) => format!("{}/{}", p, files.parent.name),
None => format!("./{}", files.parent.name),
};

println!("Downloading: {}", files.parent.name);
println!("Saving to: {}\n", output_path);

// https://rust-lang-nursery.github.io/rust-cookbook/os/external.html#redirect-both-stdout-and-stderr-of-child-process-to-the-same-file
ProcessCommand::new("curl")
.arg("-C")
.arg("-")
.arg("-o")
.arg(output_path)
.arg(url_response.url)
.stdout(Stdio::piped())
.spawn()
.expect("error while spawning curl")
.wait_with_output()
.expect("running CURL command");

println!("\nDownload finished!\n")
}
}

Ok(())
}
31 changes: 19 additions & 12 deletions src/put/oob.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
use std::collections::HashMap;

use reqwest::{blocking::Client, Error};

/// Returns a new OOB code.
pub fn get() -> Result<String, Box<dyn std::error::Error>> {
let resp = reqwest::blocking::get("https://api.put.io/v2/oauth2/oob/code?app_id=4701")?
pub fn get(client: &Client) -> Result<String, Error> {
let resp = client
.get("https://api.put.io/v2/oauth2/oob/code?app_id=4701")
.send()?
.json::<HashMap<String, String>>()?;
let code = resp.get("code").expect("fetching OOB code");
Ok(code.to_string())

let code: &String = resp.get("code").expect("fetching OOB code");

Ok(code.clone())
}

/// Returns new OAuth token if the OOB code is linked to the user's account.
pub fn check(oob_code: String) -> Result<String, Box<dyn std::error::Error>> {
let resp = reqwest::blocking::get(format!(
"https://api.put.io/v2/oauth2/oob/code/{}",
oob_code
))?
.json::<HashMap<String, String>>()?;
let token = resp.get("oauth_token").expect("deserializing OAuth token");
Ok(token.to_string())
pub fn check(client: &Client, oob_code: &String) -> Result<String, Error> {
let resp = client
.get(format!("https://api.put.io/v2/oauth2/oob/code/{oob_code}"))
.send()?
.json::<HashMap<String, String>>()?;

let token: &String = resp.get("oauth_token").expect("fetching OAuth token");

Ok(token.clone())
}
Loading

0 comments on commit 98fd433

Please sign in to comment.