From 98976de4679b8be1024a69aff9fd304ffc1054d8 Mon Sep 17 00:00:00 2001 From: tilpner Date: Fri, 3 May 2019 12:19:25 +0200 Subject: Add rest of project --- src/base62.rs | 54 +++++++++++++ src/code.html | 34 ++++++++ src/main.rs | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 src/base62.rs create mode 100644 src/code.html create mode 100644 src/main.rs (limited to 'src') 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 { + 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(x: I, base: u8) -> u128 +where I: Iterator { + 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 { + 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 @@ + + + + + {title} + + + + {code | raw} + + 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, + + /// Read from given file, instead of stdin + #[structopt(short, long, parse(from_os_str))] + file: Option, + + /// Highlight as code + #[structopt(short, long)] + code: bool, + + /// Override language used to highlight input + #[structopt(short, long)] + language: Option, + + /// Set name for paste + #[structopt(short, long)] + name: Option, + + /// 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, +} + +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 = 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> + 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) -> impl Iterator> { + 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, "
");
+    for (i, line) in input_str.lines().enumerate() {
+        writeln!(&mut html, "{}", i, i);
+    }
+    write!(&mut html, "
");
+
+    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, "{}", i, formatted_line);
+    }
+    write!(&mut html, "
"); + + 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) -> impl Iterator> { + 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) +} -- cgit v1.2.3