extern crate xdg; #[allow(unused)] use env_logger::{Env, Target}; #[allow(unused)] use log::{debug, error, info, log_enabled, warn}; use reqwest; use serde::Deserialize; use std::collections::HashMap; use std::fs; use std::io::{self, Write}; #[allow(unused)] use std::process::Command; use std::str; use toml; use url::Url; #[allow(unused)] use yaserde_derive::{YaDeserialize, YaSerialize}; use yaserde::de::from_str; #[derive(Debug, Deserialize)] pub struct Config { credentials: Credentials, paths: NextcloudPaths, } #[derive(Debug, Deserialize)] pub struct Credentials { username: String, password: Option, password_script: Option, } #[derive(Debug, Deserialize)] pub struct NextcloudPaths { root: String, target: String, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde( rename = "multistatus", prefix = "d", namespace = "d: DAV:", namespace = "nc: http://nextcloud.org/ns", namespace = "oc: http://owncloud.org/ns", namespace = "s: http://sabredav.org/ns" )] pub struct Multistatus { #[yaserde(prefix = "d")] response: Vec, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde(rename = "response", prefix = "d", namespace = "d: DAV:")] pub struct NextcloudResponse { #[yaserde(prefix = "d")] // href: Option, href: String, #[yaserde(prefix = "d")] propstat: Vec, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde(rename = "propstat", prefix = "d", namespace = "d: DAV:")] pub struct Propstat { #[yaserde(prefix = "d")] prop: Prop, #[yaserde(prefix = "d")] status: String, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde( rename = "prop", namespace = "d: DAV:", namespace = "oc: http://owncloud.org/ns" )] pub struct Prop { #[yaserde(prefix = "d", rename = "getlastmodified")] get_last_modified: String, #[yaserde(prefix = "oc")] permissions: String, #[yaserde(prefix = "d", rename = "resourcetype")] resource_type: ResourceType, #[yaserde(prefix = "d", rename = "getetag")] get_etag: String, #[yaserde(prefix = "d", rename = "getcontentlength")] get_content_length: u32, #[yaserde(prefix = "d", rename = "getcontenttype")] get_content_type: String, #[yaserde(prefix = "oc", rename = "share-types")] share_types: Vec, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde(rename = "share-types", namespace = "oc: http://owncloud.org/ns")] pub struct ShareType { #[yaserde(rename = "share-type", prefix = "oc")] share_type: u8, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde(rename = "resourcetype", namespace = "d: DAV:")] pub struct ResourceType { #[yaserde(prefix = "d")] collection: Vec, } #[derive(Default, PartialEq, Debug, YaDeserialize)] #[yaserde(rename = "collection", namespace = "d: DAV:")] pub struct Collection { #[yaserde(prefix = "d")] collection: String, } fn init() -> Result> { let xdg_dirs = xdg::BaseDirectories::with_prefix("publicise-rs").unwrap(); let config_path = xdg_dirs.find_config_file("config.toml").unwrap(); let config_file_contents = fs::read_to_string(config_path).unwrap(); let config: Config = toml::from_str(&config_file_contents).unwrap(); return Ok(config); } #[allow(unused)] async fn get_folder_contents(url_tail: &str, config: &Config) -> Result> { debug!("[get_folder_contents] Entering function..."); let root_url = Url::parse(&config.paths.root)?; let method = reqwest::Method::from_bytes(b"PROPFIND").unwrap(); let client = reqwest::Client::new(); let url = root_url.join(url_tail)?; debug!("[get_folder_contents] url: {}", url); let body = String::from( r#" "#, ); let response_text = client .request(method, url) .basic_auth("adam", config.credentials.password.as_ref()) .body(body) .send() .await? .text() .await?; debug!("[get_folder_contents] response_text: {:?}", response_text); Ok(response_text) } #[allow(unused)] async fn publicise_it(path: &str, password: &str) -> Result> { debug!("[publicise_it] Entering function..."); let url = "https://cloud.theadamcooper.com/ocs/v2.php/apps/files_sharing/api/v1/shares"; let method = reqwest::Method::POST; let client = reqwest::Client::new(); let params = [("path", path), ("shareType", "3")]; debug!("[publicise_it] url: {}", url); debug!("[publicise_it] params: {:?}", params); let response_text = client .request(method, url) .basic_auth("adam", Some(password)) .header("OCS-APIRequest", "true") .form(¶ms) .send() .await? .text() .await?; debug!("[publicise_it] response_text: {:?}", response_text); Ok(true) } #[allow(unused)] async fn traverse(mut result: Multistatus, config: &Config) -> Result> { debug!("[traverse] Entering function..."); // Initialize the indexed "pointer" let mut current_index: usize = 0; // Initialize the hashmap of visited items (by etag?) let mut visited_items = HashMap::new(); let href_key = (&mut result.response[current_index].href).clone(); visited_items.insert(href_key, true); current_index += 1; // Depth first traversal while current_index < (&mut result.response).len() { debug!("[traverse] current_index: {:?}", current_index); debug!("[traverse] current href: {}", &result.response[current_index].href); let href_key = (&mut result.response[current_index].href).clone(); // If result.response[current_index] has not been visited if !visited_items.contains_key(&href_key) { debug!("[traverse] Fresh item..."); // if it's a collection if !(&mut result.response[current_index].propstat[0].prop.resource_type.collection).is_empty() { debug!("[traverse] Collection..."); // Get the contents XML let folder_contents: String = get_folder_contents( &result.response[current_index].href, // change to mutable borrow if necessary config, ) .await .unwrap(); // Parse the contents XML into Multistatus let mut new_result: Multistatus = from_str(&String::from(folder_contents)).unwrap(); debug!("[traverse] Parsed:\n{:?}", new_result); // Append the NextcloudResponse vector to result.response result.response.append(&mut new_result.response); debug!("[traverse] new vector length: {}", &result.response.len()); } else { debug!("[traverse] Node..."); // else it's a node. if it's not public, publicise it. if !(&mut result.response[current_index].propstat[0].prop.share_types).contains(&ShareType{ share_type: 3 }) { debug!("[traverse] it's not public"); // Search for username and lop. let username = "adam"; let username_seg_string = format!("/{}/", username); let username_seg = username_seg_string.as_str(); let index = &result.response[current_index].href.find(username_seg).unwrap_or(0); let new_index = index + username.len() + 2; let new_href = &result.response[current_index].href[new_index..]; publicise_it(new_href, config.credentials.password.as_ref().unwrap()).await.unwrap(); } else { debug!("[traverse] it's already public"); } } visited_items.insert(href_key, true); debug!("[traverse] visited items: {:?}", visited_items); } else { debug!("[traverse] Already-visited item."); } current_index += 1; } Ok(true) } #[allow(unused)] fn get_password() -> Result { print!("Nextcloud password: "); io::stdout().flush().unwrap(); let mut buffer = String::new(); let stdin = io::stdin(); match stdin.read_line(&mut buffer) { Ok(_) => Ok(buffer), Err(error) => Err(error), } } #[allow(unused)] fn indent(size: usize) -> String { const INDENT: &'static str = " "; (0..size) .map(|_| INDENT) .fold(String::with_capacity(size * INDENT.len()), |r, s| r + s) } #[tokio::main] async fn main() -> std::io::Result<()> { env_logger::Builder::from_env(Env::default().default_filter_or("debug")) .target(Target::Stdout) .init(); println!("Publicise it!\n\n"); let mut config = init().unwrap(); if config.credentials.password == None { let output = Command::new(&config.credentials.password_script.as_ref().unwrap()).output().unwrap(); config.credentials.password = Some(String::from_utf8(output.stdout).unwrap()); } debug!("{:?}", &config); let full_path = &(String::from("/remote.php/dav/files/") + &config.credentials.username + "/" + &config.paths.target); let folder_contents: String = get_folder_contents(full_path, &config) .await .unwrap(); let result: Multistatus = from_str(&folder_contents).unwrap(); debug!("{:?}", result); let _ = traverse(result, &config).await.unwrap(); Ok(()) }