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