Initial commit

This commit is contained in:
IndyV 2022-10-26 22:58:36 +02:00
commit 8da5ff8f8b
9 changed files with 1813 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.vscode
/target

1388
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "sharextended"
version = "0.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eyre = "0.6.8"
toml = "0.5.9"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.86"
clap = { version = "4.0.17", features = ["derive"] }
reqwest = { version = "0.11", features = ["json"] }
ansi_term = "0.12.1"
indicatif = "0.17.1"
dialoguer = "0.10.2"
tinyfiledialogs = "3.9.1"
open = "3.0.3"
lazy_static = "1.4.0"
console = "0.15.2"
futures = "0.3.24"
directories = "4.0.1"

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# Delete ShareX online hosted images
A simple program that deletes all images that are hosted on online image hosting by sending POST requests to all DeletionURL's.
## Usage
```ps1
./sharextended.exe --help
```
### TODO
- Unwrap and expect should be replaced with proper error handling.
- Add GitHub action to build and release binaries (Windows only, since ShareX is Windows only).
## Motivation
### Why Rust?
I wanted to try out the most loved language for myself.
While it's a bit hard to get started with due to these new concepts, like ownership, borrowing, and lifetimes, I really like it.
It's a very powerful language with great error messages and many useful crates. I'm looking forward to learn more about it.
### Code
Watching video's from [No Boilerplate](https://www.youtube.com/c/NoBoilerplate) motivated me to try out Rust. On advise about your Rust toolkit, he suggested the following Clippy lints:
```ps1
cargo clippy --fix -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used
```
For now I omit the `clippy::unwrap_used`. It's not great to use unwrap as often as I do, but I use it for getting things working. I'm sure I'll get better at error handling in the future.
I tried to store the static strings in the `Cargo.toml`, but I figured it's not meant to live outside the code. Also tried to resolve `%USERPROFILE%` in the code but solved it with the `directories` crate.
I used https://transform.tools/json-to-rust-serde as a reference for the JSON structure.

2
lint.sh Normal file
View File

@ -0,0 +1,2 @@
# cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --fix -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -W clippy::expect_used

63
src/main.rs Normal file
View File

@ -0,0 +1,63 @@
mod mass_delete;
mod util;
use clap::Parser;
use dialoguer::{theme::ColorfulTheme, Select};
use eyre::Result;
#[tokio::main]
async fn main() {
let args = util::Args::parse();
let command = args.command;
match command {
Some(util::Command::MassDelete { path }) => {
mass_delete::handler(path).await.unwrap();
}
None => {
show_menu().await.unwrap();
}
}
}
async fn show_menu() -> Result<()> {
loop {
println!();
let menu_response: usize = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Pick a option (use arrow keys to select, enter to confirm)")
.items(&[
"1. Mass delete online history items",
"2. Open ShareX Website",
"3. View source code (GitHub)",
"Exit",
])
.default(0)
.interact()
.unwrap();
println!();
handle_option(menu_response).await?;
}
}
async fn handle_option(number: usize) -> Result<()> {
match number {
0 => {
mass_delete::handler(None).await?;
}
1 => {
util::open_webpage(util::SHAREX_URL.to_string().as_str());
}
2 => {
util::open_webpage(util::REPO_URL.to_string().as_str());
}
3 => {
std::process::exit(0);
}
_ => {
println!("Invalid option");
}
}
Ok(())
}

250
src/mass_delete.rs Normal file
View File

@ -0,0 +1,250 @@
use crate::util;
use ansi_term::{self, Colour};
use clap::Parser;
use directories::{BaseDirs, UserDirs};
use eyre::Result;
use serde::{Deserialize, Serialize};
use std::thread;
use std::{
io::Read,
path::{Path, PathBuf},
time::Duration,
};
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HistoryItem {
#[serde(rename = "FileName")]
pub file_name: String,
#[serde(rename = "FilePath")]
pub file_path: Option<String>,
#[serde(rename = "DateTime")]
pub date_time: String,
#[serde(rename = "Type")]
pub type_field: String,
#[serde(rename = "Host")]
pub host: String,
#[serde(rename = "Tags")]
pub tags: Option<Tags>,
#[serde(rename = "URL")]
pub url: Option<String>,
#[serde(rename = "ThumbnailURL")]
pub thumbnail_url: Option<String>,
#[serde(rename = "DeletionURL")]
pub deletion_url: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Tags {
#[serde(rename = "WindowTitle")]
pub window_title: Option<String>,
#[serde(rename = "ProcessName")]
pub process_name: String,
}
pub async fn handler(pathflag: Option<PathBuf>) -> Result<()> {
// TODO
// If a pathflag is Some() it means it's passed via a flag, use that path and skip the prompting
// Otherwise, prompt the user to select a default path (depending on type of installation), or tinyfiledialog for a path, or manual input
let path = match pathflag {
Some(pathflag) => pathflag,
None => get_path_input(),
};
let file = prompt_history_file(&path);
if !Path::new(&file).exists() {
eprintln!(
"{} The directory does not exist. Given path: {:?}",
Colour::Red.paint("Error:"),
&file
);
std::process::exit(1);
}
let history_urls = get_history_urls(&file);
delete_urls(history_urls).await?;
Ok(())
}
fn prompt_history_file(path: &Path) -> PathBuf {
// TODO: Use dialoguer select to prompt the user to select the history file, either with tinyfiledialogs or manual input
return tinyfiledialogs::open_file_dialog(
"Choose where sharex history is stored",
path.to_str().unwrap(),
Some((&["History.json", "*.json"], "History.json")),
)
.map_or_else(
|| {
eprintln!("No file selected, exiting...");
std::process::exit(1);
},
PathBuf::from,
);
}
fn get_history_urls(path: &PathBuf) -> Result<Vec<String>> {
let spinner = util::setup_spinner("Reading and parsing JSON...");
let history_json = read_history_json(&path)?;
let history_items = parse_history_json(&history_json)?;
let deletion_urls = get_deletion_urls(&history_items);
// spinner.finish_and_clear();
spinner.finish_with_message(format!("Done! {} items found", deletion_urls.len()));
Ok(deletion_urls)
}
fn get_path_input() -> PathBuf {
let args = util::Args::parse();
let path = match args.command {
Some(util::Command::MassDelete { path }) => path,
None => None,
};
let default_history_path = get_default_history_path();
match path {
Some(path) => path,
None => default_history_path.clone(),
}
}
fn get_default_history_path() -> PathBuf {
let document_directory: PathBuf = if let Some(user_dirs) = UserDirs::new() {
user_dirs.document_dir().unwrap().to_path_buf()
} else {
BaseDirs::new().unwrap().home_dir().join("Documents")
};
let default_history_path: PathBuf = document_directory.join("ShareX").join("History.json");
default_history_path
}
fn read_history_json(path: &PathBuf) -> Result<String> {
let mut file = std::fs::File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
// Since ShareX history is invalid JSON we add brackets to make it valid JSON
contents = format!("[{}]", contents);
Ok(contents)
}
fn parse_history_json(json: &str) -> Result<Vec<HistoryItem>, serde_json::Error> {
let history_items: Vec<HistoryItem> = serde_json::from_str(json)?;
Ok(history_items)
}
fn get_deletion_urls(items: &[HistoryItem]) -> Vec<String> {
items
.iter()
.filter(|item| item.deletion_url.is_some() && item.deletion_url != Some("".to_string()))
.map(|item| item.deletion_url.clone().unwrap())
.collect()
}
async fn delete_urls(deletion_urls: Result<Vec<String>>) -> Result<()> {
let deletion_urls = deletion_urls?;
let progress_bar = util::setup_progressbar(deletion_urls.len());
// progress_bar.enable_steady_tick(Duration::from_millis(500)); // This only visually updates the ticker once every 500ms instead of when the tick occurs
// ? Maybe use Rayon to parallelize the requests and run them through public proxies to prevent rate limiting?
for url in deletion_urls {
let (remaining, limit, reset) = send_deletion(&url).await?;
println!(
"Remaining: {} Limit: {} Reset: {}",
Colour::Green.paint(remaining),
Colour::Yellow.paint(limit),
Colour::Red.paint(reset)
);
progress_bar.inc(1);
thread::sleep(Duration::from_millis(100));
}
// let client = reqwest::Client::new();
// let mut futures = Vec::new();
// for url in deletion_urls {
// let future = client.delete(&url).send();
// futures.push(future);
// }
// let results = futures::future::join_all(futures).await;
// for result in results {
// match result {
// Ok(response) => {
// if response.status().is_success() {
// println!("Deleted {}", response.url());
// } else {
// eprintln!("Failed to delete {}", response.url());
// }
// }
// Err(e) => {
// eprintln!("Failed to delete {}", e);
// }
// }
// }
progress_bar.finish_with_message("Done!");
Ok(())
}
async fn send_deletion(url: &str) -> Result<(String, String, String)> {
let client = reqwest::Client::new();
let params = [("confirm", true)];
let resp = client.post(url).form(&params).send().await?;
println!("{:#?}", resp);
match resp.status() {
reqwest::StatusCode::OK => {
println!("OK");
}
reqwest::StatusCode::TOO_MANY_REQUESTS => {
println!("TOO MANY REQUESTS");
}
reqwest::StatusCode::BAD_GATEWAY => {
println!("BAD GATEWAY");
}
_ => {
println!("Not OK");
}
}
// I don't understand Rust enough so the stuff below looks kinda cursed
let headers = resp.headers().clone();
let remaining = headers
.get("x-post-rate-limit-remaining")
.unwrap()
.to_str()?
.to_owned();
let limit = headers
.get("x-post-rate-limit-limit")
.unwrap()
.to_str()?
.to_owned();
let reset = headers
.get("x-post-rate-limit-Reset")
.unwrap()
.to_str()?
.to_owned();
println!(
"Remaining: {} Limit: {} Reset: {}",
Colour::Green.paint(&remaining),
Colour::Yellow.paint(&limit),
Colour::Red.paint(&reset)
);
print!("{:?}", headers);
Ok((remaining, limit, reset))
}

2
src/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod mass_delete;
mod util;

49
src/util.rs Normal file
View File

@ -0,0 +1,49 @@
use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use std::path::PathBuf;
use std::{borrow::Cow, time::Duration};
lazy_static! {
pub static ref SHAREX_URL: &'static str = "https://getsharex.com/";
pub static ref REPO_URL: &'static str = "https://github.com/IndyV/sharextend_cli";
}
#[derive(Parser, Debug)]
#[clap(version = "1.0", author = "IndyV", about = "Delete all ShareX online history items", long_about = None)]
pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(clap::Subcommand, Clone, Debug)]
pub enum Command {
MassDelete {
#[clap(short, long)]
path: Option<PathBuf>,
},
}
pub fn setup_spinner(msg: impl Into<Cow<'static, str>>) -> ProgressBar {
let spinner: ProgressBar = ProgressBar::new_spinner().with_message(msg);
spinner.enable_steady_tick(Duration::from_millis(1000));
spinner
}
pub fn setup_progressbar(items: usize) -> ProgressBar {
let progress_bar = ProgressBar::new(items as u64);
progress_bar.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
)
.unwrap()
.progress_chars("##-"),
);
progress_bar
}
pub fn open_webpage(url: &str) {
open::that(url).unwrap_or_else(|_| panic!("Unable to open webpage {}", &url));
}