Improve handling of static files.

Use static files handling from updated ructe rather than local
handling, and use rsass rather than sass-rs and sass-sys.

Static files are now kept in the binary (no more need to access the
build directory at runtime).  Also, there is an admin command to save
the static files to a directory, for the benefit of fronting web
servers and backwards compatibility.
This commit is contained in:
Rasmus Kaj 2017-01-29 19:03:35 +01:00
parent 698fb2c23b
commit 8cd215cdc2
7 changed files with 71 additions and 76 deletions

View File

@ -6,14 +6,9 @@ authors = ["Rasmus Kaj <kaj@kth.se>"]
build = "src/build.rs" build = "src/build.rs"
[build-dependencies] [build-dependencies]
sass-rs = "*"
sass-sys = "*"
md5 = "*"
rustc-serialize = "*"
flate2 = "*"
brotli2 = "*"
diesel_codegen_syntex = { version = "*", features = ["postgres", "syntex"] } diesel_codegen_syntex = { version = "*", features = ["postgres", "syntex"] }
ructe = "*" ructe = "*"
rsass = "*"
[[bin]] [[bin]]
name = "rphotoserver" name = "rphotoserver"
@ -46,3 +41,5 @@ r2d2-diesel = "*"
djangohashers = "*" djangohashers = "*"
rand = "*" rand = "*"
memcached-rs = "^0.1" memcached-rs = "^0.1"
flate2 = "*"
brotli2 = "*"

View File

@ -4,3 +4,4 @@ pub mod readkpa;
pub mod result; pub mod result;
pub mod stats; pub mod stats;
pub mod users; pub mod users;
pub mod storestatics;

26
src/adm/storestatics.rs Normal file
View File

@ -0,0 +1,26 @@
use adm::result::Error;
use brotli2::write::BrotliEncoder;
use flate2::{Compression, FlateWriteExt};
use std::fs::{File, create_dir_all};
use std::io::prelude::*;
use std::path::Path;
pub fn to_dir(dir: &str) -> Result<(), Error> {
let dir: &Path = dir.as_ref();
try!(create_dir_all(&dir));
for s in STATICS {
try!(File::create(dir.join(s.name))
.and_then(|mut f| f.write(s.content)));
try!(File::create(dir.join(format!("{}.gz", s.name)))
.map(|f| f.gz_encode(Compression::Best))
.and_then(|mut f| f.write(s.content)));
try!(File::create(dir.join(format!("{}.br", s.name)))
.map(|f| BrotliEncoder::new(f, 11))
.and_then(|mut f| f.write(s.content)));
}
Ok(())
}
include!(concat!(env!("OUT_DIR"), "/templates/statics.rs"));

View File

@ -1,31 +1,23 @@
extern crate brotli2;
extern crate diesel_codegen_syntex as diesel_codegen; extern crate diesel_codegen_syntex as diesel_codegen;
extern crate flate2; extern crate rsass;
extern crate md5;
extern crate rustc_serialize as serialize;
extern crate sass_rs;
extern crate sass_sys;
extern crate ructe; extern crate ructe;
use brotli2::write::BrotliEncoder; use rsass::{OutputStyle, compile_scss_file};
use flate2::{Compression, FlateWriteExt}; use ructe::{compile_static_files, compile_templates};
use ructe::compile_templates;
use sass_rs::dispatcher::Dispatcher;
use sass_rs::sass_context::SassFileContext;
use serialize::base64::{self, ToBase64};
use std::env; use std::env;
use std::fs::{File, create_dir_all}; use std::fs::{File, create_dir_all};
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::thread;
fn main() { fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
prepare_diesel(&out_dir); prepare_diesel(&out_dir);
do_sassify(&out_dir); let css_dir = out_dir.join("tmpcss");
do_sassify(&css_dir);
let template_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) let template_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
.join("templates"); .join("templates");
compile_templates(&template_dir, &out_dir).unwrap(); compile_static_files(&css_dir, &out_dir).expect("statics");
compile_templates(&template_dir, &out_dir).expect("templates");
} }
pub fn prepare_diesel(out_dir: &Path) { pub fn prepare_diesel(out_dir: &Path) {
@ -38,59 +30,16 @@ pub fn prepare_diesel(out_dir: &Path) {
println!("cargo:rerun-if-changed=src/lib.in.rs"); println!("cargo:rerun-if-changed=src/lib.in.rs");
} }
pub fn do_sassify(out_dir: &Path) { pub fn do_sassify(static_dir: &Path) {
let static_dir = out_dir.join("static").join("static");
create_dir_all(&static_dir).unwrap(); create_dir_all(&static_dir).unwrap();
let css = compile("photos.scss").unwrap(); let css = compile_scss_file("photos.scss".as_ref(),
let css = css.as_bytes(); OutputStyle::Compressed).unwrap();
let filename = format!("style-{}.css", checksum_slug(&css));
File::create(static_dir.join(&filename)) File::create(static_dir.join("style.css"))
.and_then(|mut f| f.write(css)) .and_then(|mut f| f.write(&css))
.expect("Writing css"); .expect("Writing css");
File::create(static_dir.join(format!("{}.gz", filename)))
.map(|f| f.gz_encode(Compression::Best))
.and_then(|mut f| f.write(css))
.expect("Writing gzipped css");
File::create(static_dir.join(format!("{}.br", filename)))
.map(|f| BrotliEncoder::new(f, 11))
.and_then(|mut f| f.write(css))
.expect("Writing brotli compressed css");
File::create(&out_dir.join("stylelink"))
.and_then(|mut f| {
writeln!(f,
"\"<link rel='stylesheet' href='/static/{}' \
type='text/css'/>\"",
filename)
})
.expect("Writing stylelink");
// TODO Find any referenced files! // TODO Find any referenced files!
println!("cargo:rerun-if-changed=photos.scss"); println!("cargo:rerun-if-changed=photos.scss");
} }
/// A short and url-safe checksum string from string data.
fn checksum_slug(data: &[u8]) -> String {
md5::compute(data)[9..].to_base64(base64::URL_SAFE)
}
/// Setup the sass environment and compile a file.
fn compile(filename: &str) -> Result<String, String> {
let mut file_context = SassFileContext::new(filename);
// options.set_output_style(COMPRESSED) or similar, when supported.
if let Ok(mut opt) = file_context.sass_context.sass_options.write() {
unsafe {
sass_sys::sass_option_set_output_style(
opt.raw.get_mut(),
sass_sys::SASS_STYLE_COMPRESSED);
}
}
let options = file_context.sass_context.sass_options.clone();
thread::spawn(move || {
let dispatcher = Dispatcher::build(vec![], options);
while dispatcher.dispatch().is_ok() {}
});
file_context.compile()
}

View File

@ -31,7 +31,7 @@ use diesel::prelude::*;
use dotenv::dotenv; use dotenv::dotenv;
use hyper::header::{Expires, HttpDate}; use hyper::header::{Expires, HttpDate};
use nickel::{FormBody, Halt, HttpRouter, MediaType, MiddlewareResult, Nickel, use nickel::{FormBody, Halt, HttpRouter, MediaType, MiddlewareResult, Nickel,
Request, Response, StaticFilesHandler}; Request, Response};
use nickel::extensions::response::Redirect; use nickel::extensions::response::Redirect;
use nickel::status::StatusCode; use nickel::status::StatusCode;
use nickel_diesel::{DieselMiddleware, DieselRequestExtensions}; use nickel_diesel::{DieselMiddleware, DieselRequestExtensions};
@ -100,13 +100,10 @@ fn main() {
let mut server = Nickel::new(); let mut server = Nickel::new();
server.utilize(RequestLoggerMiddleware); server.utilize(RequestLoggerMiddleware);
server.get("/static/:file.:ext", wrap!(static_file: file, ext));
server.utilize(MemcacheMiddleware::new server.utilize(MemcacheMiddleware::new
(vec![("tcp://127.0.0.1:11211".into(), 1)])); (vec![("tcp://127.0.0.1:11211".into(), 1)]));
server.utilize(SessionMiddleware::new(&jwt_key())); server.utilize(SessionMiddleware::new(&jwt_key()));
// TODO This is a "build" location, not an "install" location ...
let staticdir = concat!(env!("OUT_DIR"), "/static/");
info!("Serving static files from {}", staticdir);
server.utilize(StaticFilesHandler::new(staticdir));
let dm: DieselMiddleware<PgConnection> = let dm: DieselMiddleware<PgConnection> =
DieselMiddleware::new(&dburl(), 5, Box::new(NopErrorHandler)).unwrap(); DieselMiddleware::new(&dburl(), 5, Box::new(NopErrorHandler)).unwrap();
server.utilize(dm); server.utilize(dm);
@ -308,6 +305,20 @@ fn place_all<'mw>(req: &mut Request,
query.order(place_name).load(c).expect("List places"))) query.order(place_name).load(c).expect("List places")))
} }
fn static_file<'mw>(_req: &mut Request,
mut res: Response<'mw>,
name: String,
ext: String)
-> MiddlewareResult<'mw> {
use templates::statics::StaticFile;
if let Some(s) = StaticFile::get(&format!("{}.{}", name, ext)) {
res.set(ext.parse().unwrap_or(MediaType::Bin));
res.set(Expires(HttpDate(time::now() + Duration::days(300))));
return res.send(s.content);
}
res.error(StatusCode::NotFound, "No such file")
}
fn place_one<'mw>(req: &mut Request, fn place_one<'mw>(req: &mut Request,
res: Response<'mw>, res: Response<'mw>,
tslug: String) tslug: String)

View File

@ -1,5 +1,6 @@
//! Just fooling around with different ways to count images per year. //! Just fooling around with different ways to count images per year.
extern crate rphotos; extern crate rphotos;
extern crate brotli2;
extern crate clap; extern crate clap;
extern crate chrono; extern crate chrono;
#[macro_use] #[macro_use]
@ -7,6 +8,7 @@ extern crate diesel;
extern crate djangohashers; extern crate djangohashers;
extern crate dotenv; extern crate dotenv;
extern crate env_logger; extern crate env_logger;
extern crate flate2;
extern crate rand; extern crate rand;
extern crate rexif; extern crate rexif;
#[macro_use] #[macro_use]
@ -18,7 +20,7 @@ mod adm;
mod env; mod env;
mod photosdir; mod photosdir;
use adm::{findphotos, makepublic, readkpa, users}; use adm::{findphotos, makepublic, readkpa, users, storestatics};
use adm::result::Error; use adm::result::Error;
use adm::stats::show_stats; use adm::stats::show_stats;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
@ -66,6 +68,11 @@ fn main() {
.help("Image path to make public")) .help("Image path to make public"))
.after_help("The image path(s) are relative to the \ .after_help("The image path(s) are relative to the \
image root.")) image root."))
.subcommand(SubCommand::with_name("storestatics")
.about("Store statics as files for a web server")
.arg(Arg::with_name("DIR")
.required(true)
.help("Directory to store the files in")))
.get_matches(); .get_matches();
match run(args) { match run(args) {
@ -122,6 +129,9 @@ fn run(args: ArgMatches) -> Result<(), Error> {
("userpass", Some(args)) => { ("userpass", Some(args)) => {
users::passwd(&try!(get_db()), args.value_of("USER").unwrap()) users::passwd(&try!(get_db()), args.value_of("USER").unwrap())
} }
("storestatics", Some(args)) => {
storestatics::to_dir(args.value_of("DIR").unwrap())
}
_ => Ok(println!("No subcommand given.\n\n{}", args.usage())), _ => Ok(println!("No subcommand given.\n\n{}", args.usage())),
} }
} }

View File

@ -1,5 +1,6 @@
@use ::Link; @use ::Link;
@use templates::head; @use templates::head;
@use templates::statics::style_css;
@(title: &str, lpath: Vec<Link>, user: Option<String>, content: Content) @(title: &str, lpath: Vec<Link>, user: Option<String>, content: Content)
@ -8,7 +9,7 @@
<head> <head>
<title>@title</title> <title>@title</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
@Html(include!(concat!(env!("OUT_DIR"), "/stylelink"))) <link rel="stylesheet" href="/static/@style_css.name" type="text/css"/>
</head> </head>
<body> <body>
@:head(lpath, user) @:head(lpath, user)