Initial
This commit is contained in:
commit
232a2fc2d9
10 changed files with 3315 additions and 0 deletions
74
src/engine/fs.rs
Normal file
74
src/engine/fs.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use super::Engine;
|
||||
use crate::{FileDescriptor, decrypt_deflate, encrypt_compress, salt};
|
||||
|
||||
use pathbufd::PathBufD;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{read_to_string, remove_file, write},
|
||||
io::Result,
|
||||
};
|
||||
|
||||
pub struct FsEngine;
|
||||
|
||||
impl Engine for FsEngine {
|
||||
const CHUNK_SIZE: usize = 200_000;
|
||||
|
||||
async fn auth(&mut self) -> Result<()> {
|
||||
unreachable!("Not needed");
|
||||
}
|
||||
|
||||
async fn process(&self, name: String, data: Vec<u8>) -> Result<FileDescriptor> {
|
||||
let (seed, key, data) = encrypt_compress(data);
|
||||
let mut descriptor = FileDescriptor {
|
||||
name,
|
||||
key,
|
||||
seed,
|
||||
engine_data: HashMap::new(),
|
||||
chunks: Vec::new(),
|
||||
};
|
||||
|
||||
for chunk in data.as_bytes().chunks(Self::CHUNK_SIZE) {
|
||||
let id = salt();
|
||||
self.create_chunk(&id, String::from_utf8(chunk.to_vec()).unwrap())
|
||||
.await?;
|
||||
descriptor.chunks.push(id);
|
||||
}
|
||||
|
||||
descriptor.write(PathBufD::current().join(format!("{}.toml", descriptor.name)))?;
|
||||
Ok(descriptor)
|
||||
}
|
||||
|
||||
async fn reconstruct(&self, descriptor: FileDescriptor) -> Result<()> {
|
||||
let mut encoded_string: String = String::new();
|
||||
|
||||
for chunk in descriptor.chunks {
|
||||
encoded_string += &self.get_chunk(&chunk).await?;
|
||||
}
|
||||
|
||||
let decoded = decrypt_deflate(descriptor.seed, descriptor.key, encoded_string);
|
||||
write(PathBufD::current().join(descriptor.name), decoded)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&mut self, descriptor: FileDescriptor) -> Result<()> {
|
||||
for chunk in descriptor.chunks {
|
||||
self.delete_chunk(&chunk).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_chunk(&self, id: &str) -> Result<String> {
|
||||
read_to_string(PathBufD::new().join(id))
|
||||
}
|
||||
|
||||
async fn create_chunk(&self, id: &str, data: String) -> Result<String> {
|
||||
write(PathBufD::new().join(id), data)?;
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
async fn delete_chunk(&self, id: &str) -> Result<()> {
|
||||
remove_file(PathBufD::new().join(id))
|
||||
}
|
||||
}
|
29
src/engine/mod.rs
Normal file
29
src/engine/mod.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
#![allow(async_fn_in_trait)]
|
||||
|
||||
pub mod fs;
|
||||
pub mod rentry;
|
||||
|
||||
use std::io::Result;
|
||||
|
||||
use crate::FileDescriptor;
|
||||
|
||||
/// An engine that drives remote storage.
|
||||
pub trait Engine {
|
||||
const CHUNK_SIZE: usize;
|
||||
|
||||
/// Returns authentication details (CSRF token, session token, etc).
|
||||
async fn auth(&mut self) -> Result<()>;
|
||||
/// Processes a file and returns a [`FileDescriptor`].
|
||||
async fn process(&self, name: String, data: Vec<u8>) -> Result<FileDescriptor>;
|
||||
/// Reads a [`FileDescriptor`] and recreates the file.
|
||||
async fn reconstruct(&self, descriptor: FileDescriptor) -> Result<()>;
|
||||
/// Reads a [`FileDescriptor`] and deletes the file.
|
||||
async fn delete(&mut self, descriptor: FileDescriptor) -> Result<()>;
|
||||
/// Gets the data in a chunk by its ID.
|
||||
async fn get_chunk(&self, id: &str) -> Result<String>;
|
||||
/// Creates a new chunk and sends it to remote.
|
||||
async fn create_chunk(&self, id: &str, data: String) -> Result<String>;
|
||||
/// Deletes a specific chunk from the remote. Used to clean up remote data
|
||||
/// when deleting a file.
|
||||
async fn delete_chunk(&self, id: &str) -> Result<()>;
|
||||
}
|
309
src/engine/rentry.rs
Normal file
309
src/engine/rentry.rs
Normal file
|
@ -0,0 +1,309 @@
|
|||
use super::Engine;
|
||||
use crate::{FileDescriptor, decrypt_deflate, encrypt_compress, salt};
|
||||
|
||||
use pathbufd::PathBufD;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::write,
|
||||
io::{Error, Result},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
pub struct RentryEngine {
|
||||
pub http: Client,
|
||||
pub csrf_token: Option<String>,
|
||||
}
|
||||
|
||||
impl RentryEngine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
http: Client::new(),
|
||||
csrf_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_csrf(&self, url: &str) -> String {
|
||||
println!("start: extract token {}", url);
|
||||
let body = self
|
||||
.http
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token = body
|
||||
.split("name=\"csrfmiddlewaretoken\" value=\"")
|
||||
.skip(1)
|
||||
.next()
|
||||
.unwrap()
|
||||
.split("\"")
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
println!("start: token obtained");
|
||||
token
|
||||
}
|
||||
}
|
||||
|
||||
impl Engine for RentryEngine {
|
||||
const CHUNK_SIZE: usize = 150_000; // chunks are given 8 bytes for extra characters
|
||||
|
||||
async fn auth(&mut self) -> Result<()> {
|
||||
self.csrf_token = Some(self.get_csrf("https://rentry.co").await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process(&self, name: String, data: Vec<u8>) -> Result<FileDescriptor> {
|
||||
let token = if let Some(ref token) = self.csrf_token {
|
||||
token.to_owned()
|
||||
} else {
|
||||
return Err(Error::other("no csrf token extracted"));
|
||||
};
|
||||
|
||||
let (seed, key, data) = encrypt_compress(data);
|
||||
let mut descriptor = FileDescriptor {
|
||||
name,
|
||||
key,
|
||||
seed,
|
||||
engine_data: {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("token".to_string(), token);
|
||||
data
|
||||
},
|
||||
chunks: Vec::new(),
|
||||
};
|
||||
|
||||
let chars = data.chars().collect::<Vec<char>>();
|
||||
let chunks = chars.chunks(Self::CHUNK_SIZE);
|
||||
let chunks_num = chunks.len();
|
||||
|
||||
let mut handles: Vec<JoinHandle<()>> = Vec::new();
|
||||
|
||||
for (i, chunk) in chunks.enumerate() {
|
||||
let id = salt();
|
||||
println!("cnksv: start chunk save ({}/{chunks_num})", i + 1);
|
||||
|
||||
// spawn worker thread
|
||||
let c = chunk.to_vec();
|
||||
|
||||
let mut worker_engine = Self::new();
|
||||
worker_engine.csrf_token = self.csrf_token.clone();
|
||||
|
||||
let id_c = id.clone();
|
||||
tokio::time::sleep(Duration::from_millis(750)).await; // sleep to avoid rate limit
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut b = worker_engine
|
||||
.create_chunk(&id_c, c.iter().collect())
|
||||
.await
|
||||
.expect("failed to create chunk");
|
||||
|
||||
while b.contains("e436") {
|
||||
println!(
|
||||
"\x1b[91mcnksv: upload failed (hit limit on {})\x1b[0m",
|
||||
i + 1
|
||||
);
|
||||
println!("cnksv: creation limit reached, waiting to try again");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10000)).await;
|
||||
|
||||
b = worker_engine
|
||||
.create_chunk(&id_c, c.iter().collect())
|
||||
.await
|
||||
.expect("failed to create chunk");
|
||||
}
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
descriptor.chunks.push(id.clone());
|
||||
}
|
||||
|
||||
// wait for all handles to finish
|
||||
println!("cnksv: waiting for all handles to finish");
|
||||
loop {
|
||||
let mut all_finished = true;
|
||||
|
||||
for handle in &handles {
|
||||
if !handle.is_finished() {
|
||||
all_finished = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if all_finished {
|
||||
break;
|
||||
}
|
||||
|
||||
// sleep a little to save cpu
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
}
|
||||
|
||||
// ...
|
||||
descriptor.write(PathBufD::current().join(format!("{}.toml", descriptor.name)))?;
|
||||
Ok(descriptor)
|
||||
}
|
||||
|
||||
async fn reconstruct(&self, descriptor: FileDescriptor) -> Result<()> {
|
||||
let mut encoded_string: String = String::new();
|
||||
|
||||
let chunks_num = descriptor.chunks.len();
|
||||
for (i, chunk) in descriptor.chunks.iter().enumerate() {
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
println!("cnkrd: start chunk read ({}/{chunks_num})", i + 1);
|
||||
|
||||
if let Ok(d) = self.get_chunk(&chunk).await {
|
||||
println!("cnkrd: read {} bytes", d.len());
|
||||
encoded_string += &d;
|
||||
} else {
|
||||
println!("cnkrd: waiting to retry chunk read");
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
encoded_string += &self.get_chunk(&chunk).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let decoded = decrypt_deflate(descriptor.seed, descriptor.key, encoded_string);
|
||||
write(PathBufD::current().join(descriptor.name), decoded)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&mut self, descriptor: FileDescriptor) -> Result<()> {
|
||||
self.csrf_token = Some(
|
||||
descriptor
|
||||
.engine_data
|
||||
.get("token")
|
||||
.expect("rentry engine requires engine_data.token")
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
for (i, chunk) in descriptor.chunks.iter().enumerate() {
|
||||
println!(
|
||||
"cnkde: start chunk delete ({}/{})",
|
||||
i + 1,
|
||||
descriptor.chunks.len()
|
||||
);
|
||||
|
||||
if self.delete_chunk(&chunk).await.is_err() {
|
||||
// wait and then retry
|
||||
println!("cnkde: waiting to retry chunk deletion");
|
||||
tokio::time::sleep(Duration::from_millis(750)).await;
|
||||
|
||||
if self.delete_chunk(&chunk).await.is_err() {
|
||||
println!("cnkde: failed to delete chunk {} (skipping)", i + 1);
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_chunk(&self, id: &str) -> Result<String> {
|
||||
println!("cnkrq: chunk request");
|
||||
let res = self
|
||||
.http
|
||||
.get(format!("https://rentry.co/renbin_{id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(Error::other("remote error"));
|
||||
}
|
||||
|
||||
let body = res.text().await.unwrap();
|
||||
|
||||
// this shows how useful that raw key thing is for you
|
||||
let content = body
|
||||
.split("<div><p>")
|
||||
.skip(1)
|
||||
.next()
|
||||
.unwrap()
|
||||
.split("</p></div>")
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
println!("cnkrd: chunk read");
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
async fn create_chunk(&self, id: &str, data: String) -> Result<String> {
|
||||
let token = if let Some(ref token) = self.csrf_token {
|
||||
token
|
||||
} else {
|
||||
return Err(Error::other("no csrf token extracted"));
|
||||
};
|
||||
|
||||
// create body
|
||||
let mut body_map: HashMap<&str, String> = HashMap::new();
|
||||
body_map.insert("csrfmiddlewaretoken", token.to_owned());
|
||||
body_map.insert("text", data);
|
||||
body_map.insert("metadata", String::new());
|
||||
body_map.insert("edit_code", token.to_owned());
|
||||
body_map.insert("url", format!("renbin_{id}"));
|
||||
|
||||
// ...
|
||||
let res = self
|
||||
.http
|
||||
.post("https://rentry.co")
|
||||
.form(&body_map)
|
||||
.header("Cookie", format!("csrftoken={token}"))
|
||||
.header("Referer", "https://rentry.co/")
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to create chunk");
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(Error::other("remote error"));
|
||||
}
|
||||
|
||||
println!("cnksv: chunk saved");
|
||||
Ok(res.text().await.unwrap())
|
||||
}
|
||||
|
||||
async fn delete_chunk(&self, id: &str) -> Result<()> {
|
||||
let token = if let Some(ref token) = self.csrf_token {
|
||||
token
|
||||
} else {
|
||||
return Err(Error::other("no csrf token extracted"));
|
||||
};
|
||||
|
||||
// create body
|
||||
let mut body_map: HashMap<&str, String> = HashMap::new();
|
||||
body_map.insert("csrfmiddlewaretoken", token.to_owned());
|
||||
body_map.insert("text", "renbin_delete".to_string());
|
||||
body_map.insert("metadata", String::new());
|
||||
body_map.insert("edit_code", token.to_owned());
|
||||
body_map.insert("new_edit_code", String::new());
|
||||
body_map.insert("new_url", String::new());
|
||||
body_map.insert("new_modify_code", String::new());
|
||||
body_map.insert("delete", "delete".to_string());
|
||||
|
||||
// the rentry api is one of the worst i've ever seen
|
||||
let url = format!("https://rentry.co/renbin_{id}/edit");
|
||||
|
||||
let res = self
|
||||
.http
|
||||
.post(&url)
|
||||
.form(&body_map)
|
||||
.header("Cookie", format!("csrftoken={token}"))
|
||||
.header("Referer", &url)
|
||||
.send()
|
||||
.await
|
||||
.expect("failed to delete chunk");
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(Error::other("remote error"));
|
||||
}
|
||||
|
||||
println!("cnkde: chunk deleted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
127
src/lib.rs
Normal file
127
src/lib.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
pub mod engine;
|
||||
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Nonce,
|
||||
aead::{AeadCore, AeadInPlace, KeyInit, OsRng},
|
||||
aes::cipher::typenum,
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
|
||||
use pathbufd::PathBufD as PathBuf;
|
||||
use rand::{Rng, distr::Alphanumeric, rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{read_to_string, write},
|
||||
io::{Result, prelude::*},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct FileDescriptor {
|
||||
/// The name of the file.
|
||||
pub name: String,
|
||||
/// The encryption key used for the file.
|
||||
pub key: String,
|
||||
/// The seed (nonce) used for the file.
|
||||
pub seed: String,
|
||||
/// Data needed by the engine to read this file.
|
||||
pub engine_data: HashMap<String, String>,
|
||||
/// A list of all locations to retrieve chunks from.
|
||||
/// Each chunk **must** be in the correct order.
|
||||
pub chunks: Vec<String>,
|
||||
}
|
||||
|
||||
impl FileDescriptor {
|
||||
/// Read a [`FileDescriptor`] from the given `path`.
|
||||
pub fn read(path: PathBuf) -> Self {
|
||||
toml::from_str(&read_to_string(path).expect("failed to read file"))
|
||||
.expect("failed to deserialize file")
|
||||
}
|
||||
|
||||
/// Write a [`FileDescriptor`] into the given `path`.
|
||||
pub fn write(&self, path: PathBuf) -> Result<()> {
|
||||
write(
|
||||
path,
|
||||
toml::to_string_pretty(&self).expect("failed to serialize file"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a random (not necessarily unique) 16-byte long string.
|
||||
pub fn salt() -> String {
|
||||
rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Encrypt and compress the given `data` using the given `key`.
|
||||
///
|
||||
/// # Returns
|
||||
/// `(nonce, key, encrypted as base64)`
|
||||
pub fn encrypt_compress(data: Vec<u8>) -> (String, String, String) {
|
||||
// compress
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
encoder.write_all(&data).expect("failed to compress");
|
||||
println!("encod: data compressed");
|
||||
|
||||
let mut compressed_buffer: Vec<u8> = encoder.finish().unwrap();
|
||||
|
||||
// encrypt
|
||||
let key = Aes256Gcm::generate_key(&mut OsRng);
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
|
||||
cipher
|
||||
.encrypt_in_place(&nonce, b"", &mut compressed_buffer)
|
||||
.expect("failed to encrypt");
|
||||
|
||||
println!("encod: data encrypted");
|
||||
|
||||
// return
|
||||
(
|
||||
STANDARD.encode(nonce.to_vec()),
|
||||
STANDARD.encode(key.to_vec()),
|
||||
STANDARD.encode(compressed_buffer.to_vec()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Decrypt and uncompress the given `data` using the given `key`.
|
||||
///
|
||||
/// # Returns
|
||||
/// `(key, encrypted)`
|
||||
pub fn decrypt_deflate(nonce: String, key: String, data: String) -> Vec<u8> {
|
||||
// decrypt
|
||||
// decryption must happen first since we must undo what was done previously
|
||||
let key = STANDARD.decode(key).expect("recieved invalid base64 key");
|
||||
let cipher = Aes256Gcm::new_from_slice(&key.as_slice()).unwrap();
|
||||
let nonce = Nonce::<typenum::U12>::clone_from_slice(
|
||||
STANDARD
|
||||
.decode(nonce)
|
||||
.expect("received invalid base64 nonce")
|
||||
.as_slice(),
|
||||
);
|
||||
|
||||
let data = STANDARD.decode(data).expect("received invalid base64 data");
|
||||
let mut decrypted_buffer: Vec<u8> = data;
|
||||
|
||||
cipher
|
||||
.decrypt_in_place(&nonce, b"", &mut decrypted_buffer)
|
||||
.expect("failed to decrypt");
|
||||
|
||||
println!("decod: data decrypted");
|
||||
|
||||
// compress
|
||||
let mut decoder = GzDecoder::new(decrypted_buffer.as_slice());
|
||||
|
||||
let mut decompressed_buffer: Vec<u8> = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut decompressed_buffer)
|
||||
.expect("failed to decompress");
|
||||
|
||||
println!("decod: data decompressed");
|
||||
|
||||
// return
|
||||
decompressed_buffer
|
||||
}
|
101
src/main.rs
Normal file
101
src/main.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
extern crate renbin;
|
||||
|
||||
use clap::Parser;
|
||||
use renbin::{
|
||||
FileDescriptor,
|
||||
engine::{Engine, fs::FsEngine, rentry::RentryEngine},
|
||||
};
|
||||
use std::{
|
||||
fs::{read, remove_file},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(short = 'd', long = "decode", action)]
|
||||
decode: bool,
|
||||
#[arg(short = 'x', long = "delete", action)]
|
||||
delete: bool,
|
||||
#[arg(short = 'i', long = "input")]
|
||||
path: String,
|
||||
#[arg(short = 'e', long = "engine")]
|
||||
#[clap(default_value = "fs")]
|
||||
engine: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.delete {
|
||||
// delete
|
||||
let path = PathBuf::from(args.path);
|
||||
|
||||
if args.engine == "fs" {
|
||||
FsEngine
|
||||
.delete(FileDescriptor::read(path.clone().into()))
|
||||
.await
|
||||
.expect("failed to reconstruct file");
|
||||
} else if args.engine == "rentry" {
|
||||
let mut engine = RentryEngine::new();
|
||||
engine.auth().await.expect("failed to extract csrf token");
|
||||
|
||||
engine
|
||||
.delete(FileDescriptor::read(path.clone().into()))
|
||||
.await
|
||||
.expect("failed to delete file");
|
||||
} else {
|
||||
panic!("unknown engine type");
|
||||
};
|
||||
|
||||
remove_file(path).expect("failed to delete file descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
if !args.decode {
|
||||
// encode
|
||||
let pathbuf = PathBuf::from(args.path);
|
||||
|
||||
if args.engine == "fs" {
|
||||
FsEngine
|
||||
.process(
|
||||
pathbuf.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
read(pathbuf).expect("failed to read file"),
|
||||
)
|
||||
.await
|
||||
.expect("failed to process file")
|
||||
} else if args.engine == "rentry" {
|
||||
let mut engine = RentryEngine::new();
|
||||
engine.auth().await.expect("failed to extract csrf token");
|
||||
|
||||
engine
|
||||
.process(
|
||||
pathbuf.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
read(pathbuf).expect("failed to read file"),
|
||||
)
|
||||
.await
|
||||
.expect("failed to process file")
|
||||
} else {
|
||||
panic!("unknown engine type");
|
||||
};
|
||||
} else {
|
||||
// decode
|
||||
if args.engine == "fs" {
|
||||
FsEngine
|
||||
.reconstruct(FileDescriptor::read(PathBuf::from(args.path).into()))
|
||||
.await
|
||||
.expect("failed to reconstruct file");
|
||||
} else if args.engine == "rentry" {
|
||||
let mut engine = RentryEngine::new();
|
||||
engine.auth().await.expect("failed to extract csrf token");
|
||||
|
||||
engine
|
||||
.reconstruct(FileDescriptor::read(PathBuf::from(args.path).into()))
|
||||
.await
|
||||
.expect("failed to reconstruct file");
|
||||
} else {
|
||||
panic!("unknown engine type");
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue