diff options
author | tilpner | 2019-05-03 12:19:25 +0200 |
---|---|---|
committer | tilpner | 2019-05-03 12:19:25 +0200 |
commit | 98976de4679b8be1024a69aff9fd304ffc1054d8 (patch) | |
tree | 092332217ac829af77b2e0f5c60d456c3de6134e /src | |
parent | 35f5424d81e687096c7b7bd60f839deeb5e7f876 (diff) | |
download | rpb-s3-98976de4679b8be1024a69aff9fd304ffc1054d8.tar.gz rpb-s3-98976de4679b8be1024a69aff9fd304ffc1054d8.tar.xz rpb-s3-98976de4679b8be1024a69aff9fd304ffc1054d8.zip |
Add rest of project
Diffstat (limited to 'src')
-rw-r--r-- | src/base62.rs | 54 | ||||
-rw-r--r-- | src/code.html | 34 | ||||
-rw-r--r-- | src/main.rs | 256 |
3 files changed, 344 insertions, 0 deletions
diff --git a/src/base62.rs b/src/base62.rs new file mode 100644 index 0000000..6ec1e62 --- /dev/null +++ b/src/base62.rs @@ -0,0 +1,54 @@ +use std::iter; +use itertools; + +pub const BASE: u8 = 62; +pub const ALPHABET: &'static [u8; BASE as usize] = + b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +pub fn number_to_digits(mut x: u128, base: u8) -> Vec<u8> { + let mut out = Vec::new(); + let base = base as u128; + while x > base { + out.push((x % base) as u8); + x /= base; + } + out.push(x as u8); + out.reverse(); + out +} + +pub fn digits_to_number<I>(x: I, base: u8) -> u128 +where I: Iterator<Item=u8> { + let base = base as u128; + x.fold(0, |out, digit| out * base + digit as u128) +} + +pub fn digits_to_string<'a, I>(x: I, alphabet: &'a [u8]) -> String +where I: Iterator<Item=u8> { + x.map(|d| alphabet[d as usize] as char).collect() + +} + +#[cfg(test)] +mod test { + use crate::base62::*; + + #[test] + fn test_number_to_digits() { + assert_eq!(vec![0], number_to_digits(0, 10)); + assert_eq!(vec![8], number_to_digits(8, BASE)); + assert_eq!(vec![1, 2], number_to_digits(64, BASE)); + assert_eq!(vec![6, 4], number_to_digits(64, 10)); + } + + #[test] + fn test_digits_to_number() { + assert_eq!(digits_to_number(vec![1u8, 2].into_iter(), BASE), 64); + assert_eq!(digits_to_number(vec![6u8, 4].into_iter(), 10), 64); + } + + #[test] + fn test_digits_to_string() { + assert_eq!(digits_to_string(vec![1u8, 2].into_iter(), ALPHABET), "12"); + } +} diff --git a/src/code.html b/src/code.html new file mode 100644 index 0000000..1e009d6 --- /dev/null +++ b/src/code.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <title>{title}</title> + <style> + html, body \{ + background-color: #002b36; + height: 100%; + } + :hover \{ + color: white !important; + } + .code \{ + color: #93a1a1; + } + .linenumbers \{ + -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -o-user-select: none; + user-select: none; + } + .linenumbers a \{ + color: #586e75; + text-decoration: none; + } + </style> + </head> + <body> + {code | raw} + </body> +</html> diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f1f0ae2 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,256 @@ +#[macro_use] extern crate serde_derive; + +use std::{ + env, iter, + fs::File, + io::{ self, Read, BufRead, BufReader, Write }, + iter::FromIterator, + path::PathBuf, + sync::Arc, + collections::HashMap +}; + +use itertools::{ self, Itertools }; +use url::Url; +use rand; + +use serde::{ Serialize, Deserialize }; +use serde_value::to_value; +use config::{ Config, Source }; +use structopt::{ StructOpt, clap::ArgGroup }; + +use mime_guess::{ Mime, guess_mime_type_opt }; + +use futures::{ stream, Future }; +use rusoto_core::{ + request, + Region, ByteStream, + credential::{ ProvideAwsCredentials, StaticProvider } +}; +use rusoto_s3::{ + S3, S3Client, + PutObjectRequest, + HeadObjectRequest +}; + +mod base62; + +const ONE_TRUE_THEME: &'static str = "Solarized (dark)"; + +#[derive(StructOpt, Debug, Clone, Serialize, Deserialize)] +#[structopt(raw(group = r#"ArgGroup::with_name("processor")"#))] +struct Opt { + #[structopt(short = "C", long = "config-file", parse(from_os_str))] + config_file: Option<PathBuf>, + + /// Read from given file, instead of stdin + #[structopt(short, long, parse(from_os_str))] + file: Option<PathBuf>, + + /// Highlight as code + #[structopt(short, long)] + code: bool, + + /// Override language used to highlight input + #[structopt(short, long)] + language: Option<String>, + + /// Set name for paste + #[structopt(short, long)] + name: Option<String>, + + /// Size of random identifier: will be chosen in 0..62^L + #[structopt(short = "L", long, default_value="3")] + id_length: u32, + + /// Generate longer identifier, which is harder to brute-force + #[structopt(short, long)] + private: bool, + + /// Override mimetype for paste + #[structopt(short, long)] + mime: Option<String>, +} + +fn main() { + let cfg = { + let opt = Opt::from_args(); + let mut cfg = Config::default(); + + let xdg = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap(); + let xdg_file = xdg.find_config_file("config.json"); + let cfg_file = opt.config_file.clone() + .or(env::var("PB_CONFIG_FILE").map(PathBuf::from).ok()) + .or(xdg_file); + if let Some(cfg_file) = cfg_file { + let cfg_file = cfg_file.to_str().expect("Config file has invalid path"); + cfg.merge(config::File::new(cfg_file, config::FileFormat::Json)); + } + cfg.merge(config::Environment::with_prefix("PB")).expect("Failed to read config from environment"); + cfg.merge(Config::try_from(&opt).unwrap()).expect("Failed to read config from CLI arguments"); + + if cfg.get_bool("private").unwrap_or(false) { + cfg.set("id_length", 10); + } + cfg + }; + + let region = Region::Custom { + endpoint: { + // trailing / causes problems with rusoto + let mut s = cfg.get_str("endpoint").expect("No endpoint set"); + if s.chars().last() == Some('/') { s.pop(); } + s + }, + name: cfg.get_str("region").expect("No region set") + }; + + let path = cfg.get_str("file"); + let lang = cfg.get_str("language").ok(); + let code = cfg.get_bool("code").unwrap() + || lang.is_some(); + + let input: Box<dyn BufRead + Send> = if let Ok(ref path) = path { + Box::new(BufReader::new(File::open(path).unwrap())) + } else { + let stdin = io::stdin(); + Box::new(BufReader::new(stdin)) + }; + + let data: Box<Iterator<Item=Vec<u8>> + Send> = + if code { Box::new(highlight(&cfg, input)) } + else { Box::new(chunk_iter(input)) }; + + let body = ByteStream::new(stream::iter_ok(data)); + + let dispatcher = request::HttpClient::new().expect("Unable to create rusoto http client"); + let credentials = StaticProvider::new_minimal( + cfg.get_str("access_key_id").expect("Access key not set"), + cfg.get_str("secret_access_key").expect("Secret access key not set")); + let client = S3Client::new_with(dispatcher, credentials, region); + + let bucket = cfg.get_str("bucket").expect("Bucket not set"); + let name = cfg.get_str("name").ok() + .unwrap_or_else(|| { + let len = cfg.get_int("id_length").unwrap() as u32; + (0..).map(|_| random_id(len)) + .filter(|key| !check_exists(&client, bucket.clone(), key.clone())) + .next().unwrap() + }); + + let mime = if code { String::from("text/html; charset=utf-8") } else { + cfg.get_str("mime").ok() + .or_else(|| path.ok().and_then(guess_mime_type_opt) + .as_ref().map(Mime::to_string)) + .unwrap_or_else(|| String::from("text/plain; charset=utf-8")) + }; + + let put = client.put_object(PutObjectRequest { + bucket, + key: name.clone(), + content_type: Some(mime), + body: Some(body), + ..Default::default() + }).sync().expect("Put failed"); + + println!("{}", name); +} + +static TEMPLATE: &'static str = include_str!("./code.html"); +#[derive(Serialize)] +struct Template { title: String, code: String } +fn highlight(cfg: &Config, mut input: Box<dyn BufRead + Send>) -> impl Iterator<Item=Vec<u8>> { + use syntect::{ + parsing::{ SyntaxSet, SyntaxDefinition }, + highlighting::ThemeSet, + easy::HighlightLines, + html::{ start_highlighted_html_snippet, append_highlighted_html_for_styled_line, IncludeBackground }, + util::LinesWithEndings + }; + + use tinytemplate::TinyTemplate; + + let mut input_str = String::new(); + input.read_to_string(&mut input_str); + + let mut syntax_set = SyntaxSet::load_defaults_newlines().into_builder(); + syntax_set.add(SyntaxDefinition::load_from_str(include_str!("../syntax/nix.sublime-syntax"), false, None) + .unwrap()); + let syntax_set = syntax_set.build(); + + let syntax = + if let Ok(lang) = cfg.get_str("language") { + println!("choosing {}", &lang); + syntax_set.find_syntax_by_token(&lang).expect("Failed to load syntax") + } else if let Ok(path) = cfg.get_str("file") { + println!("choosing for {}", &path); + syntax_set.find_syntax_for_file(path) + .unwrap_or_default().expect("Failed to load syntax from path") + } else { + println!("unknown language"); + syntax_set.find_syntax_by_first_line(&input_str) + .unwrap_or_else(|| syntax_set.find_syntax_plain_text()) + }; + + let theme_set = ThemeSet::load_defaults(); + let theme = theme_set.themes.get(ONE_TRUE_THEME).expect("Failed to load theme"); + + let mut highlighter = HighlightLines::new(syntax, theme); + + let mut html = Vec::new(); + let mut formatted_line = String::new(); + let (_, bg) = start_highlighted_html_snippet(theme); + write!(&mut html, "<table><tr><td unselectable=\"on\"><pre class=linenumbers>"); + for (i, line) in input_str.lines().enumerate() { + writeln!(&mut html, "<a href=\"#L{}\">{}</a>", i, i); + } + write!(&mut html, "</pre></td><td><pre class=code><code>"); + + for (i, line) in LinesWithEndings::from(&input_str).enumerate() { + let regions = highlighter.highlight(line, &syntax_set); + formatted_line.clear(); + append_highlighted_html_for_styled_line(®ions[..], IncludeBackground::IfDifferent(bg), &mut formatted_line); + if formatted_line.ends_with('\n') { formatted_line.pop(); } + write!(&mut html, "<a id=\"L{}\"></a>{}", i, formatted_line); + } + write!(&mut html, "</code></pre></td></tr></table>"); + + let mut template = TinyTemplate::new(); + template.add_template("code", TEMPLATE); + template.add_formatter("raw", tinytemplate::format_unescaped); + + let rendered = template.render("code", &Template { + title: cfg.get_str("name").unwrap_or_else(|_| String::new()), + code: String::from_utf8(html).expect("Invalid UTF8") + }).unwrap(); + + iter::once(rendered.as_bytes().to_vec()) +} + +fn chunk_iter(input: Box<dyn BufRead + Send>) -> impl Iterator<Item=Vec<u8>> { + itertools::unfold(input, |input| { + let buf = match input.fill_buf().ok() { + Some([]) => None, + Some(buf) => Some(buf.to_vec()), + None => None + }; + + if let Some(buf) = &buf { input.consume(buf.len()) } + buf + }) +} + +fn check_exists(c: &S3Client, bucket: String, key: String) -> bool { + c.head_object(HeadObjectRequest { + bucket, key, + ..Default::default() + }).sync().is_ok() +} + +fn random_id(size: u32) -> String { + use rand::distributions::{ Distribution, Uniform }; + use base62::*; + let range = Uniform::from(0..62u128.pow(size)); + let n = range.sample(&mut rand::thread_rng()); + digits_to_string(number_to_digits(n, BASE).into_iter(), ALPHABET) +} |