Compare commits

..

11 commits

Author SHA1 Message Date
54735fdf06 Merge branch '012-loose-ends' 2022-03-22 01:32:26 -04:00
3aa9b8d3d0 Add a README (#12) 2022-03-22 01:32:05 -04:00
50c36bbcbd Add WTF license (#9) 2022-03-22 01:17:19 -04:00
72062842e7 Panic if password script yields no password (#12)
This would happen, e.g., if the database is locked.
2022-03-22 01:10:21 -04:00
e77b79afae Improve URL handling (#11) 2022-03-22 00:47:02 -04:00
7d00d6f863 Remove hard-coded username instances (#9) 2022-03-21 09:30:43 -04:00
5bb7fc6eaf Clean up logging (#8) 2022-03-21 03:43:56 -04:00
3c6afa56c4 Remove comments, stray macros, unused struct references (#7) 2022-03-21 03:08:08 -04:00
703a7c4810 Configuration struct (#6)
Configuration struct stores username, password or password script, Nextcloud root, and target path
2022-03-20 04:04:24 -04:00
a7d5962d30 [wip] Grab configuration file 2022-03-18 09:42:23 -04:00
fc275d1bc9 Redirect app to new Nextcloud subdomain 2022-03-10 14:55:28 -05:00
5 changed files with 154 additions and 61 deletions

69
Cargo.lock generated
View file

@ -86,6 +86,26 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "dotenv" name = "dotenv"
version = "0.15.0" version = "0.15.0"
@ -585,8 +605,11 @@ dependencies = [
"env_logger", "env_logger",
"log", "log",
"reqwest", "reqwest",
"serde",
"tokio", "tokio",
"toml",
"url", "url",
"xdg",
"yaserde", "yaserde",
"yaserde_derive", "yaserde_derive",
] ]
@ -649,6 +672,16 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.4" version = "1.5.4"
@ -757,9 +790,23 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.131" version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ad69dfbd3e45369132cc64e6748c2d65cdfb001a2b1c232d128b4ad60561c1" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
@ -919,6 +966,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.1" version = "0.3.1"
@ -1128,6 +1184,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "xdg"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
dependencies = [
"dirs",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.4" version = "0.8.4"

View file

@ -11,7 +11,10 @@ dotenv = "0.15.0"
env_logger = "0.8.4" env_logger = "0.8.4"
log = "0.4.0" log = "0.4.0"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0.136" , features = ["derive"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
toml = "0.5.8"
url = "2.2.2" url = "2.2.2"
xdg = "2.4.1"
yaserde = "0.7.1" yaserde = "0.7.1"
yaserde_derive = "0.7.1" yaserde_derive = "0.7.1"

13
LICENSE Normal file
View file

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
### publicise-rs
_Publicise_ is an application which traverses a Nextcloud folder and creates a public share for each file.
The application was originally devised as a way to share photos with people without Instagram accounts, and as an exploration of Rust.

View file

@ -1,15 +1,37 @@
#[allow(unused)] extern crate xdg;
use env_logger::{Env, Target};
#[allow(unused)] use env_logger::Env;
use log::{debug, error, info, log_enabled, warn}; use log::debug;
use reqwest; use reqwest;
use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{self, Write}; use std::fs;
use std::process::Command;
use std::str;
use toml;
use url::Url; use url::Url;
#[allow(unused)] use yaserde_derive::YaDeserialize;
use yaserde_derive::{YaDeserialize, YaSerialize};
use yaserde::de::from_str; 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<String>,
password_script: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct NextcloudPaths {
root: String,
target: String,
}
#[derive(Default, PartialEq, Debug, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaDeserialize)]
#[yaserde( #[yaserde(
rename = "multistatus", rename = "multistatus",
@ -28,7 +50,6 @@ pub struct Multistatus {
#[yaserde(rename = "response", prefix = "d", namespace = "d: DAV:")] #[yaserde(rename = "response", prefix = "d", namespace = "d: DAV:")]
pub struct NextcloudResponse { pub struct NextcloudResponse {
#[yaserde(prefix = "d")] #[yaserde(prefix = "d")]
// href: Option<String>,
href: String, href: String,
#[yaserde(prefix = "d")] #[yaserde(prefix = "d")]
propstat: Vec<Propstat>, propstat: Vec<Propstat>,
@ -87,9 +108,19 @@ pub struct Collection {
collection: String, collection: String,
} }
async fn get_folder_contents(url_tail: &str, password: &str) -> Result<String, Box<dyn std::error::Error>> { fn init() -> Result<Config, Box<dyn std::error::Error>> {
debug!("[init] Entering function...");
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();
debug!("[init] config: {:?}", config);
return Ok(config);
}
async fn get_folder_contents(url_tail: &str, config: &Config) -> Result<String, Box<dyn std::error::Error>> {
debug!("[get_folder_contents] Entering function..."); debug!("[get_folder_contents] Entering function...");
let root_url = Url::parse("https://theadamcooper.com")?; let root_url = Url::parse(&config.paths.root)?;
let method = reqwest::Method::from_bytes(b"PROPFIND").unwrap(); let method = reqwest::Method::from_bytes(b"PROPFIND").unwrap();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let url = root_url.join(url_tail)?; let url = root_url.join(url_tail)?;
@ -110,7 +141,7 @@ async fn get_folder_contents(url_tail: &str, password: &str) -> Result<String, B
); );
let response_text = client let response_text = client
.request(method, url) .request(method, url)
.basic_auth("adam", Some(password)) .basic_auth(config.credentials.username.as_str(), config.credentials.password.as_ref())
.body(body) .body(body)
.send() .send()
.await? .await?
@ -120,9 +151,10 @@ async fn get_folder_contents(url_tail: &str, password: &str) -> Result<String, B
Ok(response_text) Ok(response_text)
} }
async fn publicise_it(path: &str, password: &str) -> Result<bool, Box<dyn std::error::Error>> { async fn publicise_it(path: &str, config: &Config) -> Result<bool, Box<dyn std::error::Error>> {
debug!("[publicise_it] Entering function..."); debug!("[publicise_it] Entering function...");
let url = "https://theadamcooper.com/nextcloud/ocs/v2.php/apps/files_sharing/api/v1/shares"; let base_url = Url::parse(config.paths.root.as_str())?;
let url = base_url.join("ocs/v2.php/apps/files_sharing/api/v1/shares")?;
let method = reqwest::Method::POST; let method = reqwest::Method::POST;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let params = [("path", path), ("shareType", "3")]; let params = [("path", path), ("shareType", "3")];
@ -130,7 +162,7 @@ async fn publicise_it(path: &str, password: &str) -> Result<bool, Box<dyn std::e
debug!("[publicise_it] params: {:?}", params); debug!("[publicise_it] params: {:?}", params);
let response_text = client let response_text = client
.request(method, url) .request(method, url)
.basic_auth("adam", Some(password)) .basic_auth(config.credentials.username.as_str(), config.credentials.password.as_ref())
.header("OCS-APIRequest", "true") .header("OCS-APIRequest", "true")
.form(&params) .form(&params)
.send() .send()
@ -141,53 +173,43 @@ async fn publicise_it(path: &str, password: &str) -> Result<bool, Box<dyn std::e
Ok(true) Ok(true)
} }
async fn traverse(mut result: Multistatus, password: &str) -> Result<bool, Box<dyn std::error::Error>> { async fn traverse(mut result: Multistatus, config: &Config) -> Result<bool, Box<dyn std::error::Error>> {
debug!("[traverse] Entering function..."); debug!("[traverse] Entering function...");
// Initialize the indexed "pointer"
let mut current_index: usize = 0; let mut current_index: usize = 0;
// Initialize the hashmap of visited items (by etag?)
let mut visited_items = HashMap::new(); let mut visited_items = HashMap::new();
let href_key = (&mut result.response[current_index].href).clone(); let href_key = (&mut result.response[current_index].href).clone();
visited_items.insert(href_key, true); visited_items.insert(href_key, true);
current_index += 1; current_index += 1;
// Depth first traversal
while current_index < (&mut result.response).len() { while current_index < (&mut result.response).len() {
debug!("[traverse] current_index: {:?}", current_index); debug!("[traverse] current_index: {:?}", current_index);
debug!("[traverse] current href: {}", &result.response[current_index].href); debug!("[traverse] current href: {}", &result.response[current_index].href);
let href_key = (&mut result.response[current_index].href).clone(); 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) { if !visited_items.contains_key(&href_key) {
debug!("[traverse] Fresh item..."); debug!("[traverse] Fresh item...");
// if it's a collection
if !(&mut result.response[current_index].propstat[0].prop.resource_type.collection).is_empty() { if !(&mut result.response[current_index].propstat[0].prop.resource_type.collection).is_empty() {
debug!("[traverse] Collection..."); debug!("[traverse] Collection...");
// Get the contents XML
let folder_contents: String = get_folder_contents( let folder_contents: String = get_folder_contents(
&result.response[current_index].href, // change to mutable borrow if necessary &result.response[current_index].href,
password, config,
) )
.await .await
.unwrap(); .unwrap();
// Parse the contents XML into Multistatus
let mut new_result: Multistatus = from_str(&String::from(folder_contents)).unwrap(); let mut new_result: Multistatus = from_str(&String::from(folder_contents)).unwrap();
debug!("[traverse] Parsed:\n{:?}", new_result); debug!("[traverse] Parsed:\n{:?}", new_result);
// Append the NextcloudResponse vector to result.response
result.response.append(&mut new_result.response); result.response.append(&mut new_result.response);
debug!("[traverse] new vector length: {}", &result.response.len()); debug!("[traverse] new vector length: {}", &result.response.len());
} else { } else {
debug!("[traverse] Node..."); 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 }) { if !(&mut result.response[current_index].propstat[0].prop.share_types).contains(&ShareType{ share_type: 3 }) {
debug!("[traverse] it's not public"); debug!("[traverse] it's not public");
// Search for username and lop. let username = config.credentials.username.as_str();
let username = "adam";
let username_seg_string = format!("/{}/", username); let username_seg_string = format!("/{}/", username);
let username_seg = username_seg_string.as_str(); let username_seg = username_seg_string.as_str();
let index = &result.response[current_index].href.find(username_seg).unwrap_or(0); let index = &result.response[current_index].href.find(username_seg).unwrap_or(0);
let new_index = index + username.len() + 2; let new_index = index + username.len() + 2;
let new_href = &result.response[current_index].href[new_index..]; let new_href = &result.response[current_index].href[new_index..];
publicise_it(new_href, password).await.unwrap(); publicise_it(new_href, config).await.unwrap();
} else { } else {
debug!("[traverse] it's already public"); debug!("[traverse] it's already public");
} }
@ -202,43 +224,28 @@ async fn traverse(mut result: Multistatus, password: &str) -> Result<bool, Box<d
Ok(true) Ok(true)
} }
fn get_password() -> Result<String, std::io::Error> {
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] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
// use yaserde::de::from_str; env_logger::Builder::from_env(Env::default()).init();
env_logger::Builder::from_env(Env::default().default_filter_or("trace"))
.target(Target::Stdout)
.init();
println!("Publicise it!\n\n"); println!("Publicise it!\n\n");
let password: String = get_password().unwrap().trim().to_string(); let mut config = init().unwrap();
if config.credentials.password == None {
let output = Command::new(&config.credentials.password_script.as_ref().unwrap()).output().unwrap();
if output.stdout.len() == 0 {
panic!("[main] Failed to acquire password from provided script.");
}
config.credentials.password = Some(String::from_utf8(output.stdout).unwrap());
}
debug!("[main] {:?}", &config);
let full_path = &(String::from("/remote.php/dav/files/") + &config.credentials.username + "/" + &config.paths.target);
let folder_contents: String = let folder_contents: String =
get_folder_contents("/nextcloud/remote.php/dav/files/adam/test_public", &password) get_folder_contents(full_path, &config)
.await .await
.unwrap(); .unwrap();
let result: Multistatus = from_str(&folder_contents).unwrap(); let result: Multistatus = from_str(&folder_contents).unwrap();
debug!("{:?}", result); debug!("[main] {:?}", result);
let _ = traverse(result, &config).await.unwrap();
let _ = traverse(result, &password).await.unwrap();
Ok(()) Ok(())
} }