This commit is contained in:
Merlijn 2025-03-08 01:54:22 +01:00
commit fd9bd154e7
Signed by: ToxicMushroom
SSH key fingerprint: SHA256:fiv+rEfOFbxc5OPNgLT1nNCL/JndxzhkKbIJLp1JcCc
5 changed files with 3009 additions and 0 deletions

356
src/main.rs Normal file
View file

@ -0,0 +1,356 @@
use anyhow::Error;
use reqwest::redirect::Policy;
use reqwest::{multipart, Client, RequestBuilder, Response, StatusCode, Url};
use serde::{Deserialize, Serialize};
use std::cmp::min;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Arc, Mutex};
use clap::Parser;
use futures_util::StreamExt;
use keyring::Entry;
use log::{debug, error, info};
use notify_rust::{Hint, Notification, NotificationHandle};
use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
use rusqlite::{Connection, OpenFlags};
use serde::de::DeserializeOwned;
use wl_clipboard_rs::copy::{ClipboardType, MimeType, Options, Source};
type GodriveToken = String;
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
struct GodriveUploadObj {
name: String,
description: String,
overwrite: bool,
/// Nr of bytes
size: u64,
permissions: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct GodriveSharePermissions {
can_edit: bool,
can_download: bool,
can_upload: bool,
}
impl GodriveSharePermissions {
fn download_only() -> Self {
GodriveSharePermissions {
can_edit: false,
can_download: true,
can_upload: false,
}
}
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// File to upload
#[clap(short, long)]
file: PathBuf,
/// Godrive server endpoint (e.g. https://godrive.example.com:443)
#[clap(long)]
host: String,
/// Embedder server endpoint (e.g. https://d.example.com:443)
#[clap(long)]
embed_host: Option<String>,
/// Authentication token
#[clap(long)]
auth_token: GodriveToken,
}
struct CommunicationContext {
client: Client,
host: String,
}
#[tokio::main]
async fn main() {
let copied_success = Arc::new(Mutex::new(false));
let args = Args::parse();
let client = Client::new();
let com_ctx = CommunicationContext {
client,
host: args.host,
};
// Show initial notification, indicating upload start
let mut notification_handle = Notification::new()
.summary("Starting upload")
.body(
format!(
"progress: 0%\nbar: ◻◻◻◻◻◻◻◻◻◻\nfile: {}",
args.file.display()
)
.as_str(),
)
.hint(Hint::Resident(true))
.show()
.unwrap();
let uploaded_file = post_upload(
&com_ctx,
vec![&args.file],
&args.auth_token,
&mut notification_handle,
)
.await
.pop()
.unwrap();
let ext = uploaded_file.name.rsplit_once(".").map(|t| t.1.to_string());
let share_link = godrive_get_share_link(&com_ctx, &args.auth_token, uploaded_file).await;
let share_url = Url::parse(share_link.as_str()).expect("Share link is not a url");
let mut opt_id = None;
if let Some(segment) = share_url.path_segments() {
opt_id = segment.collect::<Vec<&str>>().get(1).map(|s| s.to_string())
}
let raw_link = if let Some(godrive_share_id) = opt_id {
if let Some(embed_host) = args.embed_host {
format!(
"{}/{}/file.{}",
embed_host,
godrive_share_id,
ext.unwrap_or("mp4".to_string())
)
} else {
share_link
}
} else {
share_link
};
// Show finished notification
notification_handle
.summary("Upload to godrive finished")
.body(raw_link.as_str())
.action("open", "Open link")
.action("copy", "Copy link")
.action("dismiss", "Dismiss")
.hint(Hint::Resident(true));
notification_handle.update();
let notification_id = notification_handle.id();
notification_handle.wait_for_action(|action| {
match action {
"copy" => {
let opts = Options::new()
.foreground(true)
.clipboard(ClipboardType::Both)
.clone();
match opts.copy(
Source::Bytes(raw_link.into_bytes().into()),
MimeType::Autodetect,
) {
Ok(_) => {
println!("Copied successfully");
let copy_success_mutex = copied_success.clone();
let mut copy_success_guard = copy_success_mutex.lock().unwrap();
*copy_success_guard = true;
}
Err(e) => eprintln!("Failed to copy share link: {e:?}"),
}
}
"open" => {
Command::new("xdg-open")
.arg(raw_link.as_str())
.output()
.expect("failed to call xdg-open");
println!("Opened successfully");
}
"default" => println!("you clicked \"default\""),
"clicked" => println!("don hector salamanca, kill them"),
// here "__closed" is a hard coded keyword
"__closed" => println!("the notification was closed"),
_ => (),
}
});
if *copied_success.lock().unwrap() {
Notification::new()
.id(notification_id)
.summary("Copied!")
.show()
.expect("AAA");
} else {
Notification::new()
.id(notification_id)
.summary("Error")
.body("Something went wrong during the copy, check logs.")
.show()
.expect("AAAA");
}
}
async fn fire<T: DeserializeOwned>(req_builder: RequestBuilder) -> Result<T, Error> {
let resp = req_builder.send().await;
match resp {
Ok(resp) => {
let resp_dbg = format!("{:?}", resp);
let result_text = resp
.text()
.await
.expect("apparently they didnt send us text");
let result = serde_json::from_str((&result_text).as_str());
match result {
Ok(good) => Ok(good),
Err(bad) => {
eprintln!(
"{:?}, serialization error or something, idk\nResp: {:?}\nBody: {:?}",
bad, resp_dbg, result_text
);
Err(Error::msg(bad))
}
}
}
Err(error) => {
eprintln!("{:?}, networking error or something, idk", error);
return Err(Error::from(error));
}
}
}
async fn godrive_get_share_link(
ctx: &CommunicationContext,
auth_token: &GodriveToken,
file: GodriveUploadObj,
) -> String {
let share_obj = ctx
.client
.post(format!("{}/{}?action=share", &ctx.host, file.name))
.header(
AUTHORIZATION,
HeaderValue::from_str(&auth_token.as_str()).unwrap(),
)
.send()
.await
.expect("Being able to send, maybe you have no internet poopyface")
.text()
.await
.expect("A response lol");
let pattern = format!("{}/share/", &ctx.host);
match share_obj.find(pattern.as_str()) {
None => {
eprintln!("{share_obj:?}");
panic!("No share link found.");
}
Some(idx) => {
let resp_from_link = &share_obj[idx..];
let terminato_pos = resp_from_link.find("\"").unwrap();
resp_from_link[..terminato_pos].to_string()
}
}
}
async fn post_upload(
ctx: &CommunicationContext,
files: Vec<&PathBuf>,
auth_token: &GodriveToken,
notification_handle: &mut NotificationHandle,
) -> Vec<GodriveUploadObj> {
let mut form = multipart::Form::new();
let mut file_infos = vec![];
for (i, file) in files.iter().enumerate() {
match fs::metadata(file) {
Ok(metadata) => {
let content_length = metadata.len();
file_infos.push(GodriveUploadObj {
name: file
.file_name()
.expect("You provided stupid file path")
.to_str()
.expect("You provided strange file path.")
.to_string(),
description: "".to_string(),
overwrite: false,
size: content_length,
permissions: vec![],
});
}
Err(err) => {
eprintln!("Failed to get metadata from file-{i} {file:?}: {err}");
return vec![];
}
};
}
form = form.text("json", serde_json::to_string(&file_infos).unwrap());
for (i, file) in files.iter().enumerate() {
form = match form.file(format!("file-{i}"), file.clone()).await {
Ok(f) => f,
Err(e) => {
eprintln!("Failed to add multipart file-{i} {file:?}: {e}");
return vec![];
}
};
}
let boundary = form.boundary();
let content_type = format!("multipart/form-data; boundary={}", boundary).clone();
let content_length = form.compute_length();
let body = form.stream();
let mut uploaded = 0;
let block_count = 10usize;
let mut block_prog = 0usize;
let total_size = body
.content_length()
.unwrap_or(file_infos.iter().map(|i| i.size).sum());
let mut body_stream = body.into_stream();
let notif_id = notification_handle.id();
let async_fios = file_infos.clone();
let async_stream = async_stream::stream! {
while let Some(chunk) = body_stream.next().await {
if let Ok(chunk) = &chunk {
let new = min(uploaded + (chunk.len() as u64), total_size);
uploaded = new;
let new_block_prog = ((uploaded as f64 / total_size as f64) * block_count as f64).floor() as usize;
if new_block_prog > block_prog {
block_prog = new_block_prog;
let files = async_fios.iter().map(|i| &i.name).collect::<Vec<&String>>();
Notification::new()
.id(notif_id)
.summary("upload progress")
.body(format!("bar: {}{}\nfile(s): {:?}", "".repeat(block_prog), "".repeat(block_count - block_prog), files).as_str())
.hint(Hint::Resident(true))
.show().expect("WORK");
}
}
yield chunk;
}
};
let mut builder = ctx
.client
.post(format!("{}", ctx.host))
.header(
AUTHORIZATION,
HeaderValue::from_str(&auth_token.as_str()).unwrap(),
)
.body(reqwest::Body::wrap_stream(async_stream))
.header(CONTENT_TYPE, content_type);
if let Some(length) = content_length {
builder = builder.header(CONTENT_LENGTH, length);
}
match builder.send().await {
Ok(yay) => {
if !StatusCode::is_success(&yay.status()) {
eprintln!("error\nResp: {:?}", &yay);
return vec![];
}
file_infos
}
Err(err) => {
eprintln!("{}", err);
return vec![];
}
}
}