Compare commits

..

31 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
98efdaaa82 Using href in visited_items hashmap 2022-03-07 09:41:57 -05:00
cb0173c440 [wip] More debugging statements
Added more debugging statements because maybe tree is not getting fully traversed
2022-03-07 03:38:27 -05:00
d9e0a55a84 [wip] Not sure exactly what's happening, but the POST requests are returning 200 2022-03-06 18:45:16 -05:00
7966f61894 [wip] Runs without panic, but doesn't actually update, apparently. 2022-02-27 23:14:28 -05:00
6292b5dddf [wip] No errors, may have a working version here. 2022-02-27 03:13:49 -05:00
062d973d8a [wip] Mostly debugging statements 2022-02-26 18:51:01 -05:00
b331363d09 [wip] Still no errors 2022-02-20 01:25:34 -05:00
ebc6eaf6ee [wip] Committing before removing Option<T>
When I tried to add the next bit here, the borrow checker
objected to mutable borrows in lines 136 and 147, borrows
it has no problem with now. So I'm going to try to get this
working without `Option<T>` first.
2022-02-17 03:02:08 -05:00
e735a3eeec [wip] No errors at this point 2022-02-16 09:30:46 -05:00
2298b04eec [wip] Committing before starting over
Committing before duplicating the file and rewriting
it line by line with more careful attention to the
borrow checker.
2022-02-16 09:17:43 -05:00
06e60dce14 [wip] Change of approach: moving the Multistatus
Move the Multistatus value from the main function into the traverse
function, as it will not be used in the main function afterwards.
In the traverse function, the struct is now borrowed as mutable,
which fixes the append function, but we'll probably need to use
references in all the match arms.
2022-02-15 09:31:15 -05:00
1c8e868afd [wip] Committing now when down to one error
Committing now when down to one error, and before zapping a bunch of
ampersands.
2022-02-13 21:01:33 -05:00
3b69ca58cd [wip] Commit before appeasing the borrow checker
To appease the borrow checker, we'll move the whole struct from main()
into tree_traversal().
2022-01-27 08:46:55 -05:00
767d77ffcc [wip] Commit before string refactor 2022-01-27 08:45:27 -05:00
e034933fc0 [wip] Committing before match refactor 2022-01-21 08:53:35 -05:00
d00bc97478 [wip] Starting depth-first tree traversal 2022-01-16 19:24:39 -05:00
5bc4b24fd9 Merge branch 'yaserde' 2022-01-08 17:12:56 -05:00
4f086f4717 Fix .gitignore 2022-01-08 17:12:00 -05:00
22f3ea6353 FIXED #3: Parse XML Response 2022-01-08 16:51:20 -05:00
aed09ac9d8 [wip] No compilation errors 2022-01-07 09:20:17 -05:00
6 changed files with 354 additions and 76 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/target /target
.env .env
Cargo.lock Cargo.lock
scratchpad/
*.bak

119
Cargo.lock generated
View file

@ -11,6 +11,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "array_tool"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f8cb5d814eb646a863c4f24978cff2880c4be96ad8cde2c0f0678732902e271"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -80,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"
@ -214,6 +240,15 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -565,12 +600,18 @@ dependencies = [
name = "publicise-rs" name = "publicise-rs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"array_tool",
"dotenv", "dotenv",
"env_logger", "env_logger",
"log", "log",
"reqwest", "reqwest",
"serde",
"tokio", "tokio",
"xml-rs", "toml",
"url",
"xdg",
"yaserde",
"yaserde_derive",
] ]
[[package]] [[package]]
@ -631,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"
@ -739,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"
@ -901,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"
@ -948,6 +1022,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
@ -1104,8 +1184,41 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "yaserde"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2776ec5bb20e76d89268e87e1ea66c078b94f55e9771e4d648adda3019f87fc"
dependencies = [
"log",
"xml-rs",
]
[[package]]
name = "yaserde_derive"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c0b0a4701f203ebaecce4971a6bb8575aa07b617bdc39ddfc6ffeff3a38530d"
dependencies = [
"heck",
"log",
"proc-macro2",
"quote",
"syn",
"xml-rs",
]

View file

@ -6,9 +6,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
array_tool = "1.0.3"
dotenv = "0.15.0" 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"] }
xml-rs = "0.8.4" toml = "0.5.8"
url = "2.2.2"
xdg = "2.4.1"
yaserde = "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,29 +1,132 @@
extern crate xml; extern crate xdg;
#[allow(unused)] use env_logger::Env;
// use dotenv::dotenv; use log::debug;
use env_logger::{ Env, Target };
#[allow(unused)]
use log::{debug, info, log_enabled, warn, error};
use reqwest; use reqwest;
#[allow(unused)] use serde::Deserialize;
use std::io::{self, BufReader, Write}; use std::collections::HashMap;
#[allow(unused)] use std::fs;
use xml::reader::{EventReader, XmlEvent}; use std::process::Command;
use std::str;
use toml;
use url::Url;
use yaserde_derive::YaDeserialize;
use yaserde::de::from_str;
#[derive(Debug, Deserialize)]
#[allow(unused)] pub struct Config {
fn publicise() { credentials: Credentials,
paths: NextcloudPaths,
} }
async fn get_folder_contents(password: String, url_tail: &str) -> Result<String, Box<dyn std::error::Error>> { #[derive(Debug, Deserialize)]
debug!("Entering: get_folder_contents()"); pub struct Credentials {
let root_url = "https://theadamcooper.com"; username: String,
password: Option<String>,
password_script: Option<String>,
}
#[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<NextcloudResponse>,
}
#[derive(Default, PartialEq, Debug, YaDeserialize)]
#[yaserde(rename = "response", prefix = "d", namespace = "d: DAV:")]
pub struct NextcloudResponse {
#[yaserde(prefix = "d")]
href: String,
#[yaserde(prefix = "d")]
propstat: Vec<Propstat>,
}
#[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<ShareType>,
}
#[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<Collection>,
}
#[derive(Default, PartialEq, Debug, YaDeserialize)]
#[yaserde(rename = "collection", namespace = "d: DAV:")]
pub struct Collection {
#[yaserde(prefix = "d")]
collection: String,
}
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...");
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 = format!("{}{}", root_url, url_tail); let url = root_url.join(url_tail)?;
debug!("url: {}", url); debug!("[get_folder_contents] url: {}", url);
let body = String::from(r#"<?xml version="1.0" encoding="UTF-8"?> let body = String::from(
r#"<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:"> <d:propfind xmlns:d="DAV:">
<d:prop xmlns:oc="http://owncloud.org/ns"> <d:prop xmlns:oc="http://owncloud.org/ns">
<d:getlastmodified/> <d:getlastmodified/>
@ -34,79 +137,115 @@ async fn get_folder_contents(password: String, url_tail: &str) -> Result<String,
<d:getetag/> <d:getetag/>
<oc:share-types/> <oc:share-types/>
</d:prop> </d:prop>
</d:propfind>"#); </d:propfind>"#,
let response_text = client.request(method, url) );
.basic_auth("adam", Some(password)) let response_text = client
.request(method, url)
.basic_auth(config.credentials.username.as_str(), config.credentials.password.as_ref())
.body(body) .body(body)
.send() .send()
.await? .await?
.text() .text()
.await?; .await?;
debug!("{:?}", response_text); debug!("[get_folder_contents] response_text: {:?}", response_text);
Ok(response_text) Ok(response_text)
} }
fn get_password() -> Result<String, std::io::Error> { async fn publicise_it(path: &str, config: &Config) -> Result<bool, Box<dyn std::error::Error>> {
print!("Nextcloud password: "); debug!("[publicise_it] Entering function...");
io::stdout().flush().unwrap(); let base_url = Url::parse(config.paths.root.as_str())?;
let mut buffer = String::new(); let url = base_url.join("ocs/v2.php/apps/files_sharing/api/v1/shares")?;
let stdin = io::stdin(); let method = reqwest::Method::POST;
match stdin.read_line(&mut buffer) { let client = reqwest::Client::new();
Ok(_) => Ok(buffer), let params = [("path", path), ("shareType", "3")];
Err(error) => Err(error), debug!("[publicise_it] url: {}", url);
} debug!("[publicise_it] params: {:?}", params);
let response_text = client
.request(method, url)
.basic_auth(config.credentials.username.as_str(), config.credentials.password.as_ref())
.header("OCS-APIRequest", "true")
.form(&params)
.send()
.await?
.text()
.await?;
debug!("[publicise_it] response_text: {:?}", response_text);
Ok(true)
} }
fn indent(size: usize) -> String { async fn traverse(mut result: Multistatus, config: &Config) -> Result<bool, Box<dyn std::error::Error>> {
const INDENT: &'static str = " "; debug!("[traverse] Entering function...");
(0..size).map(|_| INDENT) let mut current_index: usize = 0;
.fold(String::with_capacity(size*INDENT.len()), |r, s| r + s) 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;
fn parse_xml(xml_string: String) { while current_index < (&mut result.response).len() {
let xml_str = &xml_string; debug!("[traverse] current_index: {:?}", current_index);
let parser = EventReader::from_str(xml_str); debug!("[traverse] current href: {}", &result.response[current_index].href);
let mut depth = 0; let href_key = (&mut result.response[current_index].href).clone();
let mut resourcetype_element = false; if !visited_items.contains_key(&href_key) {
let mut share_type_element = false; debug!("[traverse] Fresh item...");
for e in parser { if !(&mut result.response[current_index].propstat[0].prop.resource_type.collection).is_empty() {
match e { debug!("[traverse] Collection...");
Ok(XmlEvent::StartElement { name, .. }) => { let folder_contents: String = get_folder_contents(
println!("{}+{}", indent(depth), name); &result.response[current_index].href,
depth += 1; config,
if name.local_name.matches("resourcetype").collect::<Vec<&str>>().len() > 0 { )
println!("{} Element: resourcetype", indent(depth)); .await
resourcetype_element = true; .unwrap();
let mut new_result: Multistatus = from_str(&String::from(folder_contents)).unwrap();
debug!("[traverse] Parsed:\n{:?}", new_result);
result.response.append(&mut new_result.response);
debug!("[traverse] new vector length: {}", &result.response.len());
} else {
debug!("[traverse] Node...");
if !(&mut result.response[current_index].propstat[0].prop.share_types).contains(&ShareType{ share_type: 3 }) {
debug!("[traverse] it's not public");
let username = config.credentials.username.as_str();
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).await.unwrap();
} else {
debug!("[traverse] it's already public");
} }
} }
Ok(XmlEvent::Characters(s)) => { visited_items.insert(href_key, true);
println!("{} {}", indent(depth), s); debug!("[traverse] visited items: {:?}", visited_items);
} } else {
Ok(XmlEvent::EndElement { name }) => { debug!("[traverse] Already-visited item.");
depth -= 1;
println!("{}-{}", indent(depth), name);
}
Err(e) => {
println!("Error: {}", e);
break;
}
_ => {}
} }
current_index += 1;
} }
Ok(true)
} }
#[tokio::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("trace")).target(Target::Stdout).init(); env_logger::Builder::from_env(Env::default()).init();
println!("Publicise it!"); println!("Publicise it!\n\n");
let password = get_password().unwrap().trim().to_string();
debug!("Received password: {}[END]", password);
let folder_contents = get_folder_contents(password, "/nextcloud/remote.php/dav/files/adam/test_public").await.unwrap(); let mut config = init().unwrap();
debug!("{:?}", folder_contents); 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);
parse_xml(folder_contents); 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!("[main] {:?}", result);
let _ = traverse(result, &config).await.unwrap();
Ok(()) Ok(())
} }
// TO_BE_DELETED: Testing removal of SSH password.