init
This commit is contained in:
commit
fd9bd154e7
5 changed files with 3009 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
2617
Cargo.lock
generated
Normal file
2617
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "godrive-fileshare"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
reqwest = { git = "https://github.com/ToxicMushroom/reqwest.git", default-features = false, features = ["cookies", "http2", "json", "gzip", "rustls-tls-native-roots", "multipart", "stream"] }
|
||||||
|
anyhow = { version = "1.0.89" }
|
||||||
|
serde = { version = "^1.0.210", features = ["derive"] }
|
||||||
|
serde_json = "^1.0.128"
|
||||||
|
|
||||||
|
tokio = { version = "^1.40.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
|
futures = "^0.3.30"
|
||||||
|
futures-util = { version = "^0.3.30", features = ["sink"] }
|
||||||
|
|
||||||
|
clap = { version = "^4.5.19", features = ["derive", "env"] }
|
||||||
|
log = "0.4.22"
|
||||||
|
notify-rust = "4.11.3"
|
||||||
|
wl-clipboard-rs = "0.9.1"
|
||||||
|
async-stream = "0.3.6"
|
||||||
14
LICENSE
Normal file
14
LICENSE
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
Copyright (C) 2024 ToxicMushroom
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
356
src/main.rs
Normal file
356
src/main.rs
Normal 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![];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue