aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/base62.rs54
-rw-r--r--src/code.html34
-rw-r--r--src/main.rs256
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(&regions[..], 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)
+}