2020-07-24 21:00:00 +00:00
// Contains an embedded version of livereload-js 3.2.4
2017-06-07 09:25:36 +00:00
//
// Copyright (c) 2010-2012 Andrey Tarantsov
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2019-12-31 15:20:28 +00:00
use std ::fs ::{ read_dir , remove_dir_all } ;
2020-10-03 14:43:02 +00:00
use std ::net ::{ SocketAddrV4 , TcpListener } ;
2020-07-29 18:08:25 +00:00
use std ::path ::{ Path , PathBuf } ;
2017-03-06 10:35:56 +00:00
use std ::sync ::mpsc ::channel ;
use std ::thread ;
2018-10-31 07:18:57 +00:00
use std ::time ::{ Duration , Instant } ;
2017-03-03 08:12:40 +00:00
2019-12-31 15:20:28 +00:00
use hyper ::header ;
2021-01-05 21:06:52 +00:00
use hyper ::server ::Server ;
2019-12-31 15:20:28 +00:00
use hyper ::service ::{ make_service_fn , service_fn } ;
2021-01-05 21:06:52 +00:00
use hyper ::{ Body , Method , Request , Response , StatusCode } ;
2021-01-15 20:36:07 +00:00
use mime_guess ::from_path as mimetype_from_path ;
2019-12-31 15:20:28 +00:00
2018-10-31 07:18:57 +00:00
use chrono ::prelude ::* ;
use notify ::{ watcher , RecursiveMode , Watcher } ;
use ws ::{ Message , Sender , WebSocket } ;
2018-01-22 17:11:25 +00:00
2019-02-09 18:54:46 +00:00
use errors ::{ Error as ZolaError , Result } ;
2019-12-21 21:52:39 +00:00
use globset ::GlobSet ;
2020-09-21 16:02:37 +00:00
use relative_path ::{ RelativePath , RelativePathBuf } ;
2020-07-24 21:00:00 +00:00
use site ::sass ::compile_sass ;
2020-08-16 16:39:04 +00:00
use site ::{ Site , SITE_CONTENT } ;
2018-03-14 21:03:06 +00:00
use utils ::fs ::copy_file ;
2017-03-06 10:35:56 +00:00
2019-12-21 21:52:39 +00:00
use crate ::console ;
2020-08-16 16:39:04 +00:00
use std ::ffi ::OsStr ;
2017-03-08 04:21:45 +00:00
#[ derive(Debug, PartialEq) ]
enum ChangeKind {
Content ,
Templates ,
2019-08-24 17:05:02 +00:00
Themes ,
2017-03-08 04:21:45 +00:00
StaticFiles ,
2017-07-06 13:19:15 +00:00
Sass ,
2018-01-12 10:50:29 +00:00
Config ,
2017-03-08 04:21:45 +00:00
}
2020-07-29 18:08:25 +00:00
#[ derive(Debug, PartialEq) ]
enum WatchMode {
Required ,
Optional ,
2020-08-16 16:39:04 +00:00
Condition ( bool ) ,
2020-07-29 18:08:25 +00:00
}
2019-12-31 15:20:28 +00:00
static METHOD_NOT_ALLOWED_TEXT : & [ u8 ] = b " Method Not Allowed " ;
static NOT_FOUND_TEXT : & [ u8 ] = b " Not Found " ;
2020-07-24 19:37:57 +00:00
// This is dist/livereload.min.js from the LiveReload.js v3.2.4 release
2018-09-30 19:15:09 +00:00
const LIVE_RELOAD : & str = include_str! ( " livereload.js " ) ;
2017-03-06 10:35:56 +00:00
2021-01-05 21:06:52 +00:00
async fn handle_request ( req : Request < Body > , mut root : PathBuf ) -> Result < Response < Body > > {
2020-09-21 16:02:37 +00:00
let mut path = RelativePathBuf ::new ( ) ;
2021-01-07 18:16:31 +00:00
// https://zola.discourse.group/t/percent-encoding-for-slugs/736
let decoded = match percent_encoding ::percent_decode_str ( req . uri ( ) . path ( ) ) . decode_utf8 ( ) {
Ok ( d ) = > d ,
Err ( _ ) = > return Ok ( not_found ( ) ) ,
} ;
2020-09-21 16:02:37 +00:00
2021-01-07 18:16:31 +00:00
for c in decoded . split ( '/' ) {
2020-09-21 16:02:37 +00:00
path . push ( c ) ;
}
2019-12-31 15:20:28 +00:00
// livereload.js is served using the LIVE_RELOAD str, not a file
2020-08-16 16:39:04 +00:00
if path = = " livereload.js " {
2019-12-31 15:20:28 +00:00
if req . method ( ) = = Method ::GET {
return Ok ( livereload_js ( ) ) ;
} else {
return Ok ( method_not_allowed ( ) ) ;
}
}
2018-04-26 21:14:37 +00:00
2020-09-21 16:02:37 +00:00
if let Some ( content ) = SITE_CONTENT . read ( ) . unwrap ( ) . get ( & path ) {
2021-05-09 13:57:44 +00:00
return Ok ( in_memory_content ( & path , content ) ) ;
2020-07-24 21:00:00 +00:00
}
2021-01-05 21:06:52 +00:00
// Handle only `GET`/`HEAD` requests
match * req . method ( ) {
Method ::HEAD | Method ::GET = > { }
_ = > return Ok ( method_not_allowed ( ) ) ,
}
// Handle only simple path requests
if req . uri ( ) . scheme_str ( ) . is_some ( ) | | req . uri ( ) . host ( ) . is_some ( ) {
return Ok ( not_found ( ) ) ;
}
2021-04-07 19:47:15 +00:00
// Remove the first slash from the request path
2021-01-05 21:06:52 +00:00
// otherwise `PathBuf` will interpret it as an absolute path
2021-02-13 12:07:01 +00:00
root . push ( & decoded [ 1 .. ] ) ;
2021-04-07 19:47:15 +00:00
let metadata = tokio ::fs ::metadata ( root . as_path ( ) ) . await ? ;
if metadata . is_dir ( ) {
// if root is a directory, append index.html to try to read that instead
root . push ( " index.html " ) ;
} ;
2021-01-15 20:36:07 +00:00
let result = tokio ::fs ::read ( & root ) . await ;
2021-01-05 21:06:52 +00:00
let contents = match result {
Err ( err ) = > match err . kind ( ) {
std ::io ::ErrorKind ::NotFound = > return Ok ( not_found ( ) ) ,
std ::io ::ErrorKind ::PermissionDenied = > {
return Ok ( Response ::builder ( )
. status ( StatusCode ::FORBIDDEN )
. body ( Body ::empty ( ) )
. unwrap ( ) )
}
_ = > panic! ( " {} " , err ) ,
} ,
Ok ( contents ) = > contents ,
2019-06-18 23:05:00 +00:00
} ;
2021-01-15 20:36:07 +00:00
Ok ( Response ::builder ( )
. status ( StatusCode ::OK )
2021-03-17 08:11:02 +00:00
. header (
header ::CONTENT_TYPE ,
mimetype_from_path ( & root ) . first_or_octet_stream ( ) . essence_str ( ) ,
)
. header ( header ::ACCESS_CONTROL_ALLOW_ORIGIN , " * " )
2021-01-15 20:36:07 +00:00
. body ( Body ::from ( contents ) )
. unwrap ( ) )
2019-12-31 15:20:28 +00:00
}
fn livereload_js ( ) -> Response < Body > {
Response ::builder ( )
. header ( header ::CONTENT_TYPE , " text/javascript " )
. status ( StatusCode ::OK )
. body ( LIVE_RELOAD . into ( ) )
. expect ( " Could not build livereload.js response " )
}
2021-05-09 13:57:44 +00:00
fn in_memory_content ( path : & RelativePathBuf , content : & str ) -> Response < Body > {
let content_type = match path . extension ( ) {
Some ( ext ) = > {
match ext {
" xml " = > " text/xml " ,
" json " = > " application/json " ,
_ = > " text/html " ,
}
} ,
None = > " text/html " ,
} ;
2020-07-24 21:00:00 +00:00
Response ::builder ( )
2021-05-09 13:57:44 +00:00
. header ( header ::CONTENT_TYPE , content_type )
2020-07-24 21:00:00 +00:00
. status ( StatusCode ::OK )
. body ( content . to_owned ( ) . into ( ) )
. expect ( " Could not build HTML response " )
}
2019-12-31 15:20:28 +00:00
fn method_not_allowed ( ) -> Response < Body > {
Response ::builder ( )
. header ( header ::CONTENT_TYPE , " text/plain " )
. status ( StatusCode ::METHOD_NOT_ALLOWED )
. body ( METHOD_NOT_ALLOWED_TEXT . into ( ) )
. expect ( " Could not build Method Not Allowed response " )
2018-04-26 21:14:37 +00:00
}
2017-03-06 10:35:56 +00:00
2021-01-05 21:06:52 +00:00
fn not_found ( ) -> Response < Body > {
let not_found_path = RelativePath ::new ( " 404.html " ) ;
let content = SITE_CONTENT . read ( ) . unwrap ( ) . get ( not_found_path ) . cloned ( ) ;
2020-08-16 16:39:04 +00:00
if let Some ( body ) = content {
return Response ::builder ( )
. header ( header ::CONTENT_TYPE , " text/html " )
. status ( StatusCode ::NOT_FOUND )
. body ( body . into ( ) )
. expect ( " Could not build Not Found response " ) ;
2019-12-31 15:20:28 +00:00
}
2020-08-16 16:39:04 +00:00
// Use a plain text response when we can't find the body of the 404
2019-12-31 15:20:28 +00:00
Response ::builder ( )
. header ( header ::CONTENT_TYPE , " text/plain " )
. status ( StatusCode ::NOT_FOUND )
. body ( NOT_FOUND_TEXT . into ( ) )
. expect ( " Could not build Not Found response " )
2017-03-06 10:35:56 +00:00
}
2020-12-22 20:35:15 +00:00
fn rebuild_done_handling ( broadcaster : & Sender , res : Result < ( ) > , reload_path : & str ) {
2017-03-10 11:39:58 +00:00
match res {
Ok ( _ ) = > {
2020-12-22 20:35:15 +00:00
broadcaster
. send ( format! (
r #"
{ {
" command " : " reload " ,
2020-12-23 09:37:05 +00:00
" path " : { } ,
2020-12-22 20:35:15 +00:00
" originalPath " : " " ,
" liveCSS " : true ,
" liveImg " : true ,
" protocol " : [ " http://livereload.com/protocols/official-7 " ]
} } " #,
2020-12-23 09:37:05 +00:00
serde_json ::to_string ( & reload_path ) . unwrap ( )
2020-12-22 20:35:15 +00:00
) )
. unwrap ( ) ;
2018-10-31 07:18:57 +00:00
}
Err ( e ) = > console ::unravel_errors ( " Failed to build the site " , & e ) ,
2017-03-10 11:39:58 +00:00
}
}
2019-04-20 10:50:34 +00:00
fn create_new_site (
2020-01-21 19:52:24 +00:00
root_dir : & Path ,
2018-10-31 07:18:57 +00:00
interface : & str ,
2020-08-16 16:39:04 +00:00
interface_port : u16 ,
2020-10-03 14:43:02 +00:00
output_dir : Option < & Path > ,
2018-10-31 07:18:57 +00:00
base_url : & str ,
2020-05-23 09:55:45 +00:00
config_file : & Path ,
2019-08-24 20:23:08 +00:00
include_drafts : bool ,
2020-08-16 16:39:04 +00:00
ws_port : Option < u16 > ,
2018-10-31 07:18:57 +00:00
) -> Result < ( Site , String ) > {
2020-01-21 19:52:24 +00:00
let mut site = Site ::new ( root_dir , config_file ) ? ;
2017-03-25 07:12:58 +00:00
2020-08-16 16:39:04 +00:00
let base_address = format! ( " {} : {} " , base_url , interface_port ) ;
let address = format! ( " {} : {} " , interface , interface_port ) ;
2020-09-22 10:22:26 +00:00
2018-02-02 20:35:04 +00:00
let base_url = if site . config . base_url . ends_with ( '/' ) {
2018-02-02 16:18:07 +00:00
format! ( " http:// {} / " , base_address )
2017-03-20 10:00:00 +00:00
} else {
2018-02-02 16:18:07 +00:00
format! ( " http:// {} " , base_address )
2017-03-20 10:00:00 +00:00
} ;
2018-02-02 16:18:07 +00:00
2020-08-16 16:39:04 +00:00
site . enable_serve_mode ( ) ;
2018-02-02 20:35:04 +00:00
site . set_base_url ( base_url ) ;
2020-10-03 14:43:02 +00:00
if let Some ( output_dir ) = output_dir {
site . set_output_path ( output_dir ) ;
}
2019-08-24 20:23:08 +00:00
if include_drafts {
site . include_drafts ( ) ;
}
2017-03-21 07:57:00 +00:00
site . load ( ) ? ;
2020-08-16 16:39:04 +00:00
if let Some ( p ) = ws_port {
site . enable_live_reload_with_port ( p ) ;
} else {
site . enable_live_reload ( interface_port ) ;
}
2017-05-12 14:10:21 +00:00
console ::notify_site_size ( & site ) ;
console ::warn_about_ignored_pages ( & site ) ;
2017-03-06 10:35:56 +00:00
site . build ( ) ? ;
2018-01-12 10:50:29 +00:00
Ok ( ( site , address ) )
}
2018-10-31 07:18:57 +00:00
pub fn serve (
2020-01-21 19:52:24 +00:00
root_dir : & Path ,
2018-10-31 07:18:57 +00:00
interface : & str ,
2020-08-16 16:39:04 +00:00
interface_port : u16 ,
2020-10-03 14:43:02 +00:00
output_dir : Option < & Path > ,
2018-10-31 07:18:57 +00:00
base_url : & str ,
2020-05-23 09:55:45 +00:00
config_file : & Path ,
2019-07-04 21:42:37 +00:00
open : bool ,
2019-08-24 20:23:08 +00:00
include_drafts : bool ,
2020-08-16 16:39:04 +00:00
fast_rebuild : bool ,
2018-10-31 07:18:57 +00:00
) -> Result < ( ) > {
2018-01-12 10:50:29 +00:00
let start = Instant ::now ( ) ;
2020-02-10 19:48:52 +00:00
let ( mut site , address ) = create_new_site (
root_dir ,
interface ,
2020-08-16 16:39:04 +00:00
interface_port ,
2020-02-10 19:48:52 +00:00
output_dir ,
base_url ,
config_file ,
include_drafts ,
2020-08-16 16:39:04 +00:00
None ,
2020-02-10 19:48:52 +00:00
) ? ;
2017-05-12 14:10:21 +00:00
console ::report_elapsed_time ( start ) ;
2017-03-06 10:35:56 +00:00
2020-09-28 07:36:16 +00:00
// Stop right there if we can't bind to the address
let bind_address : SocketAddrV4 = address . parse ( ) . unwrap ( ) ;
if ( TcpListener ::bind ( & bind_address ) ) . is_err ( ) {
2020-11-21 11:38:43 +00:00
return Err ( format! ( " Cannot start server on address {} . " , address ) . into ( ) ) ;
2020-09-28 07:36:16 +00:00
}
2021-03-07 12:57:41 +00:00
let config_filename = config_file . file_name ( ) . unwrap ( ) . to_str ( ) . unwrap_or ( " config.toml " ) ;
2021-03-04 18:51:33 +00:00
2020-07-29 18:08:25 +00:00
// An array of (path, bool, bool) where the path should be watched for changes, and the boolean value
// indicates whether this file/folder must exist for zola serve to operate
2020-08-16 16:39:04 +00:00
let watch_this = vec! [
2021-03-04 18:51:33 +00:00
( config_filename , WatchMode ::Required ) ,
2020-07-29 18:08:25 +00:00
( " content " , WatchMode ::Required ) ,
( " sass " , WatchMode ::Condition ( site . config . compile_sass ) ) ,
( " static " , WatchMode ::Optional ) ,
( " templates " , WatchMode ::Optional ) ,
2020-08-16 16:39:04 +00:00
( " themes " , WatchMode ::Condition ( site . config . theme . is_some ( ) ) ) ,
] ;
2020-07-29 18:08:25 +00:00
2017-05-01 09:11:18 +00:00
// Setup watchers
let ( tx , rx ) = channel ( ) ;
2019-01-05 10:02:46 +00:00
let mut watcher = watcher ( tx , Duration ::from_secs ( 1 ) ) . unwrap ( ) ;
2017-10-25 12:49:54 +00:00
2020-07-29 18:08:25 +00:00
// We watch for changes on the filesystem for every entry in watch_this
// Will fail if either:
// - the path is mandatory but does not exist (eg. config.toml)
// - the path exists but has incorrect permissions
// watchers will contain the paths we're actually watching
let mut watchers = Vec ::new ( ) ;
for ( entry , mode ) in watch_this {
let watch_path = root_dir . join ( entry ) ;
let should_watch = match mode {
WatchMode ::Required = > true ,
WatchMode ::Optional = > watch_path . exists ( ) ,
2020-12-14 21:22:58 +00:00
WatchMode ::Condition ( b ) = > b & & watch_path . exists ( ) ,
2020-07-29 18:08:25 +00:00
} ;
if should_watch {
watcher
. watch ( root_dir . join ( entry ) , RecursiveMode ::Recursive )
2020-09-16 10:55:41 +00:00
. map_err ( | e | ZolaError ::chain ( format! ( " Can't watch ` {} ` for changes in folder ` {} `. Does it exist, and do you have correct permissions? " , entry , root_dir . display ( ) ) , e ) ) ? ;
2020-07-29 18:08:25 +00:00
watchers . push ( entry . to_string ( ) ) ;
}
2019-08-24 17:05:02 +00:00
}
2020-08-16 16:39:04 +00:00
let ws_port = site . live_reload ;
let ws_address = format! ( " {} : {} " , interface , ws_port . unwrap ( ) ) ;
2020-10-03 14:43:02 +00:00
let output_path = site . output_path . clone ( ) ;
2017-03-06 10:35:56 +00:00
2018-05-27 05:19:01 +00:00
// output path is going to need to be moved later on, so clone it for the
// http closure to avoid contention.
let static_root = output_path . clone ( ) ;
2020-12-22 20:35:15 +00:00
let broadcaster = {
2018-11-01 22:20:35 +00:00
thread ::spawn ( move | | {
2019-12-31 15:20:28 +00:00
let addr = address . parse ( ) . unwrap ( ) ;
2021-01-05 21:06:52 +00:00
let rt = tokio ::runtime ::Builder ::new_current_thread ( )
2019-12-31 15:20:28 +00:00
. enable_all ( )
. build ( )
. expect ( " Could not build tokio runtime " ) ;
rt . block_on ( async {
let make_service = make_service_fn ( move | _ | {
let static_root = static_root . clone ( ) ;
async {
Ok ::< _ , hyper ::Error > ( service_fn ( move | req | {
handle_request ( req , static_root . clone ( ) )
} ) )
}
} ) ;
let server = Server ::bind ( & addr ) . serve ( make_service ) ;
println! ( " Web server is available at http:// {} \n " , & address ) ;
if open {
if let Err ( err ) = open ::that ( format! ( " http:// {} " , & address ) ) {
eprintln! ( " Failed to open URL in your browser: {} " , err ) ;
}
2019-07-04 21:42:37 +00:00
}
2019-12-31 15:20:28 +00:00
server . await . expect ( " Could not start web server " ) ;
} ) ;
2018-11-01 22:20:35 +00:00
} ) ;
2019-12-31 15:20:28 +00:00
2018-11-01 22:20:35 +00:00
// The websocket for livereload
let ws_server = WebSocket ::new ( | output : Sender | {
move | msg : Message | {
if msg . into_text ( ) . unwrap ( ) . contains ( " \" hello \" " ) {
return output . send ( Message ::text (
r #"
{
" command " : " hello " ,
" protocols " : [ " http://livereload.com/protocols/official-7 " ] ,
" serverName " : " Zola "
}
" #,
) ) ;
}
Ok ( ( ) )
2017-05-12 12:15:50 +00:00
}
2018-11-01 22:20:35 +00:00
} )
. unwrap ( ) ;
2020-09-22 09:59:57 +00:00
2018-11-01 22:20:35 +00:00
let broadcaster = ws_server . broadcaster ( ) ;
2020-09-22 09:59:57 +00:00
let ws_server = ws_server
. bind ( & * ws_address )
2020-09-22 10:22:26 +00:00
. map_err ( | _ | format! ( " Cannot bind to address {} for the websocket server. Maybe the port is already in use? " , & ws_address ) ) ? ;
2020-09-22 09:59:57 +00:00
2018-11-01 22:20:35 +00:00
thread ::spawn ( move | | {
2020-09-22 09:59:57 +00:00
ws_server . run ( ) . unwrap ( ) ;
2018-11-01 22:20:35 +00:00
} ) ;
2020-09-22 09:59:57 +00:00
2020-12-22 20:35:15 +00:00
broadcaster
2018-11-01 22:20:35 +00:00
} ;
2020-08-16 16:39:04 +00:00
println! ( " Listening for changes in {} {{ {} }} " , root_dir . display ( ) , watchers . join ( " , " ) ) ;
2018-05-27 05:19:01 +00:00
2017-03-25 06:52:51 +00:00
println! ( " Press Ctrl+C to stop \n " ) ;
2018-01-22 17:11:25 +00:00
// Delete the output folder on ctrl+C
ctrlc ::set_handler ( move | | {
2019-12-23 08:43:08 +00:00
match remove_dir_all ( & output_path ) {
Ok ( ( ) ) = > ( ) ,
Err ( e ) = > println! ( " Errored while deleting output folder: {} " , e ) ,
}
2018-01-22 17:11:25 +00:00
::std ::process ::exit ( 0 ) ;
2018-10-31 07:18:57 +00:00
} )
. expect ( " Error setting Ctrl-C handler " ) ;
2017-03-06 10:35:56 +00:00
use notify ::DebouncedEvent ::* ;
2018-11-10 21:23:37 +00:00
let reload_sass = | site : & Site , path : & Path , partial_path : & Path | {
let msg = if path . is_dir ( ) {
format! ( " -> Directory in `sass` folder changed {} " , path . display ( ) )
} else {
format! ( " -> Sass file changed {} " , path . display ( ) )
} ;
console ::info ( & msg ) ;
2019-01-25 00:47:30 +00:00
rebuild_done_handling (
& broadcaster ,
2020-07-24 21:00:00 +00:00
compile_sass ( & site . base_path , & site . output_path ) ,
2019-01-25 00:47:30 +00:00
& partial_path . to_string_lossy ( ) ,
) ;
2018-11-10 21:23:37 +00:00
} ;
2020-08-16 16:39:04 +00:00
let reload_templates = | site : & mut Site , path : & Path | {
rebuild_done_handling ( & broadcaster , site . reload_templates ( ) , & path . to_string_lossy ( ) ) ;
} ;
2018-11-10 21:23:37 +00:00
let copy_static = | site : & Site , path : & Path , partial_path : & Path | {
// Do nothing if the file/dir was deleted
if ! path . exists ( ) {
return ;
}
let msg = if path . is_dir ( ) {
format! ( " -> Directory in `static` folder changed {} " , path . display ( ) )
} else {
format! ( " -> Static file changed {} " , path . display ( ) )
} ;
console ::info ( & msg ) ;
2019-01-25 00:47:30 +00:00
if path . is_dir ( ) {
rebuild_done_handling (
& broadcaster ,
site . copy_static_directories ( ) ,
& path . to_string_lossy ( ) ,
) ;
} else {
rebuild_done_handling (
& broadcaster ,
2019-07-19 09:10:28 +00:00
copy_file (
& path ,
& site . output_path ,
& site . static_path ,
site . config . hard_link_static ,
) ,
2019-01-25 00:47:30 +00:00
& partial_path . to_string_lossy ( ) ,
) ;
2018-11-10 21:23:37 +00:00
}
} ;
2020-02-05 08:13:14 +00:00
let recreate_site = | | match create_new_site (
root_dir ,
interface ,
2020-08-16 16:39:04 +00:00
interface_port ,
2020-02-05 08:13:14 +00:00
output_dir ,
base_url ,
config_file ,
include_drafts ,
2020-08-16 16:39:04 +00:00
ws_port ,
2020-02-05 08:13:14 +00:00
) {
Ok ( ( s , _ ) ) = > {
rebuild_done_handling ( & broadcaster , Ok ( ( ) ) , " /x.js " ) ;
Some ( s )
}
Err ( e ) = > {
console ::error ( & format! ( " {} " , e ) ) ;
None
}
} ;
2017-03-06 10:35:56 +00:00
loop {
match rx . recv ( ) {
2017-03-08 04:21:45 +00:00
Ok ( event ) = > {
2020-11-21 11:38:43 +00:00
let can_do_fast_reload = ! matches! ( event , Remove ( _ ) ) ;
2021-03-07 12:57:41 +00:00
2020-08-16 16:39:04 +00:00
match event {
2019-05-27 17:51:33 +00:00
// Intellij does weird things on edit, chmod is there to count those changes
// https://github.com/passcod/notify/issues/150#issuecomment-494912080
2020-08-16 16:39:04 +00:00
Rename ( _ , path ) | Create ( path ) | Write ( path ) | Remove ( path ) | Chmod ( path ) = > {
2019-08-01 08:18:42 +00:00
if is_ignored_file ( & site . config . ignored_content_globset , & path ) {
continue ;
}
2021-02-02 20:31:17 +00:00
2021-02-28 21:30:56 +00:00
if is_temp_file ( & path ) {
2018-11-10 21:23:37 +00:00
continue ;
}
2021-02-02 20:31:17 +00:00
2020-08-16 16:39:04 +00:00
// We only care about changes in non-empty folders
if path . is_dir ( ) & & is_folder_empty ( & path ) {
continue ;
}
2018-11-10 21:23:37 +00:00
println! (
" Change detected @ {} " ,
Local ::now ( ) . format ( " %Y-%m-%d %H:%M:%S " ) . to_string ( )
) ;
let start = Instant ::now ( ) ;
2021-03-04 18:51:33 +00:00
match detect_change_kind ( & root_dir , & path , & config_filename ) {
2018-11-10 21:23:37 +00:00
( ChangeKind ::Content , _ ) = > {
console ::info ( & format! ( " -> Content changed {} " , path . display ( ) ) ) ;
2020-08-16 16:39:04 +00:00
if fast_rebuild {
if can_do_fast_reload {
let filename = path
. file_name ( )
. unwrap_or_else ( | | OsStr ::new ( " " ) )
. to_string_lossy ( ) ;
let res = if filename = = " _index.md " {
site . add_and_render_section ( & path )
} else if filename . ends_with ( " .md " ) {
site . add_and_render_page ( & path )
} else {
// an asset changed? a folder renamed?
// should we make it smarter so it doesn't reload the whole site?
Err ( " dummy " . into ( ) )
} ;
if res . is_err ( ) {
if let Some ( s ) = recreate_site ( ) {
site = s ;
}
} else {
rebuild_done_handling (
& broadcaster ,
res ,
& path . to_string_lossy ( ) ,
) ;
}
} else {
// Should we be smarter than that? Is it worth it?
if let Some ( s ) = recreate_site ( ) {
site = s ;
}
}
2020-11-21 11:38:43 +00:00
} else if let Some ( s ) = recreate_site ( ) {
site = s ;
2020-08-16 16:39:04 +00:00
}
}
( ChangeKind ::Templates , partial_path ) = > {
let msg = if path . is_dir ( ) {
format! (
" -> Directory in `templates` folder changed {} " ,
path . display ( )
)
} else {
format! ( " -> Template changed {} " , path . display ( ) )
} ;
console ::info ( & msg ) ;
// A shortcode changed, we need to rebuild everything
if partial_path . starts_with ( " /templates/shortcodes " ) {
if let Some ( s ) = recreate_site ( ) {
site = s ;
}
} else {
println! ( " Reloading only template " ) ;
// A normal template changed, no need to re-render Markdown.
reload_templates ( & mut site , & path )
}
2018-10-31 07:18:57 +00:00
}
2018-11-10 21:23:37 +00:00
( ChangeKind ::StaticFiles , p ) = > copy_static ( & site , & path , & p ) ,
( ChangeKind ::Sass , p ) = > reload_sass ( & site , & path , & p ) ,
2019-08-24 17:05:02 +00:00
( ChangeKind ::Themes , _ ) = > {
2020-08-16 16:39:04 +00:00
console ::info ( " -> Themes changed. " ) ;
2020-02-05 08:13:14 +00:00
if let Some ( s ) = recreate_site ( ) {
site = s ;
}
2019-08-24 20:23:08 +00:00
}
2018-01-12 10:50:29 +00:00
( ChangeKind ::Config , _ ) = > {
2020-08-16 16:39:04 +00:00
console ::info ( " -> Config changed. The browser needs to be refreshed to make the changes visible. " ) ;
2020-02-05 08:13:14 +00:00
if let Some ( s ) = recreate_site ( ) {
site = s ;
}
2018-01-12 10:50:29 +00:00
}
2017-03-08 04:21:45 +00:00
} ;
2017-05-12 14:10:21 +00:00
console ::report_elapsed_time ( start ) ;
2017-03-06 10:35:56 +00:00
}
2017-03-08 04:21:45 +00:00
_ = > { }
2017-03-06 10:35:56 +00:00
}
2018-10-31 07:18:57 +00:00
}
2017-03-25 06:52:51 +00:00
Err ( e ) = > console ::error ( & format! ( " Watch error: {:?} " , e ) ) ,
2017-03-06 10:35:56 +00:00
} ;
}
}
2019-08-01 08:18:42 +00:00
fn is_ignored_file ( ignored_content_globset : & Option < GlobSet > , path : & Path ) -> bool {
match ignored_content_globset {
Some ( gs ) = > gs . is_match ( path ) ,
2019-08-10 16:53:16 +00:00
None = > false ,
2019-08-01 08:18:42 +00:00
}
}
2017-03-25 06:52:51 +00:00
/// Returns whether the path we received corresponds to a temp file created
/// by an editor or the OS
2017-03-06 10:35:56 +00:00
fn is_temp_file ( path : & Path ) -> bool {
let ext = path . extension ( ) ;
match ext {
Some ( ex ) = > match ex . to_str ( ) . unwrap ( ) {
" swp " | " swx " | " tmp " | " .DS_STORE " = > true ,
// jetbrains IDE
x if x . ends_with ( " jb_old___ " ) = > true ,
x if x . ends_with ( " jb_tmp___ " ) = > true ,
x if x . ends_with ( " jb_bak___ " ) = > true ,
2021-02-28 21:30:56 +00:00
// vim & jetbrains
2017-03-10 13:19:36 +00:00
x if x . ends_with ( '~' ) = > true ,
2017-03-06 10:35:56 +00:00
_ = > {
if let Some ( filename ) = path . file_stem ( ) {
// emacs
2018-09-13 14:57:38 +00:00
let name = filename . to_str ( ) . unwrap ( ) ;
name . starts_with ( '#' ) | | name . starts_with ( " .# " )
2017-03-06 10:35:56 +00:00
} else {
false
}
}
} ,
2018-10-31 07:18:57 +00:00
None = > true ,
2017-03-08 04:21:45 +00:00
}
}
/// Detect what changed from the given path so we have an idea what needs
/// to be reloaded
2021-03-04 18:51:33 +00:00
fn detect_change_kind ( pwd : & Path , path : & Path , config_filename : & str ) -> ( ChangeKind , PathBuf ) {
2018-08-04 19:47:45 +00:00
let mut partial_path = PathBuf ::from ( " / " ) ;
2018-08-05 05:59:56 +00:00
partial_path . push ( path . strip_prefix ( pwd ) . unwrap_or ( path ) ) ;
2018-06-20 15:43:24 +00:00
2021-03-04 18:51:33 +00:00
let mut partial_config_path = PathBuf ::from ( " / " ) ;
partial_config_path . push ( config_filename ) ;
2018-08-04 19:47:45 +00:00
let change_kind = if partial_path . starts_with ( " /templates " ) {
2017-03-08 04:21:45 +00:00
ChangeKind ::Templates
2019-08-24 17:05:02 +00:00
} else if partial_path . starts_with ( " /themes " ) {
ChangeKind ::Themes
2018-08-04 19:47:45 +00:00
} else if partial_path . starts_with ( " /content " ) {
2017-03-08 04:21:45 +00:00
ChangeKind ::Content
2018-08-04 19:47:45 +00:00
} else if partial_path . starts_with ( " /static " ) {
2017-03-08 04:21:45 +00:00
ChangeKind ::StaticFiles
2018-08-04 19:47:45 +00:00
} else if partial_path . starts_with ( " /sass " ) {
2017-07-06 13:19:15 +00:00
ChangeKind ::Sass
2021-03-04 18:51:33 +00:00
} else if partial_path = = partial_config_path {
2018-01-12 10:50:29 +00:00
ChangeKind ::Config
2017-03-08 04:21:45 +00:00
} else {
2018-08-04 19:47:45 +00:00
unreachable! ( " Got a change in an unexpected path: {} " , partial_path . display ( ) ) ;
2017-03-08 04:21:45 +00:00
} ;
2018-08-04 19:47:45 +00:00
( change_kind , partial_path )
2017-03-08 04:21:45 +00:00
}
2018-11-10 21:23:37 +00:00
/// Check if the directory at path contains any file
fn is_folder_empty ( dir : & Path ) -> bool {
// Can panic if we don't have the rights I guess?
2018-12-29 10:17:43 +00:00
let files : Vec < _ > =
read_dir ( dir ) . expect ( " Failed to read a directory to see if it was empty " ) . collect ( ) ;
files . is_empty ( )
2018-11-10 21:23:37 +00:00
}
2017-03-08 04:21:45 +00:00
#[ cfg(test) ]
mod tests {
2018-08-04 19:47:45 +00:00
use std ::path ::{ Path , PathBuf } ;
2017-03-08 04:21:45 +00:00
2018-10-31 07:18:57 +00:00
use super ::{ detect_change_kind , is_temp_file , ChangeKind } ;
2017-03-08 04:21:45 +00:00
#[ test ]
2017-05-20 15:00:41 +00:00
fn can_recognize_temp_files ( ) {
let test_cases = vec! [
2017-03-08 04:21:45 +00:00
Path ::new ( " hello.swp " ) ,
Path ::new ( " hello.swx " ) ,
Path ::new ( " .DS_STORE " ) ,
Path ::new ( " hello.tmp " ) ,
Path ::new ( " hello.html.__jb_old___ " ) ,
Path ::new ( " hello.html.__jb_tmp___ " ) ,
Path ::new ( " hello.html.__jb_bak___ " ) ,
Path ::new ( " hello.html~ " ) ,
Path ::new ( " #hello.html " ) ,
] ;
2017-05-20 15:00:41 +00:00
for t in test_cases {
2017-03-08 04:21:45 +00:00
assert! ( is_temp_file ( & t ) ) ;
}
2017-03-06 10:35:56 +00:00
}
2017-03-08 04:21:45 +00:00
#[ test ]
2017-05-20 15:00:41 +00:00
fn can_detect_kind_of_changes ( ) {
let test_cases = vec! [
2017-03-09 07:34:12 +00:00
(
2018-08-04 19:47:45 +00:00
( ChangeKind ::Templates , PathBuf ::from ( " /templates/hello.html " ) ) ,
2018-10-31 07:18:57 +00:00
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/templates/hello.html " ) ,
2021-03-04 18:51:33 +00:00
" config.toml " ,
2017-03-09 07:34:12 +00:00
) ,
2019-08-24 17:05:02 +00:00
(
( ChangeKind ::Themes , PathBuf ::from ( " /themes/hello.html " ) ) ,
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/themes/hello.html " ) ,
2021-03-04 18:51:33 +00:00
" config.toml " ,
2019-08-24 17:05:02 +00:00
) ,
2017-03-09 07:34:12 +00:00
(
2018-08-04 19:47:45 +00:00
( ChangeKind ::StaticFiles , PathBuf ::from ( " /static/site.css " ) ) ,
2018-10-31 07:18:57 +00:00
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/static/site.css " ) ,
2021-03-04 18:51:33 +00:00
" config.toml " ,
2017-03-09 07:34:12 +00:00
) ,
(
2018-08-04 19:47:45 +00:00
( ChangeKind ::Content , PathBuf ::from ( " /content/posts/hello.md " ) ) ,
2018-10-31 07:18:57 +00:00
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/content/posts/hello.md " ) ,
2021-03-04 18:51:33 +00:00
" config.toml " ,
2017-03-09 07:34:12 +00:00
) ,
2017-11-20 23:05:37 +00:00
(
2018-08-04 19:47:45 +00:00
( ChangeKind ::Sass , PathBuf ::from ( " /sass/print.scss " ) ) ,
2018-10-31 07:18:57 +00:00
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/sass/print.scss " ) ,
2021-03-04 18:51:33 +00:00
" config.toml " ,
2018-01-12 10:50:29 +00:00
) ,
(
2018-08-04 19:47:45 +00:00
( ChangeKind ::Config , PathBuf ::from ( " /config.toml " ) ) ,
2018-10-31 07:18:57 +00:00
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/config.toml " ) ,
2021-03-04 18:51:33 +00:00
" config.toml " ,
) ,
(
( ChangeKind ::Config , PathBuf ::from ( " /config.staging.toml " ) ) ,
Path ::new ( " /home/vincent/site " ) ,
Path ::new ( " /home/vincent/site/config.staging.toml " ) ,
" config.staging.toml " ,
2018-01-12 10:50:29 +00:00
) ,
2017-03-08 04:21:45 +00:00
] ;
2021-03-04 18:51:33 +00:00
for ( expected , pwd , path , config_filename ) in test_cases {
assert_eq! ( expected , detect_change_kind ( & pwd , & path , & config_filename ) ) ;
2017-03-08 04:21:45 +00:00
}
}
2018-08-04 19:47:45 +00:00
#[ test ]
2018-08-04 20:28:39 +00:00
#[ cfg(windows) ]
2018-08-04 19:47:45 +00:00
fn windows_path_handling ( ) {
let expected = ( ChangeKind ::Templates , PathBuf ::from ( " /templates/hello.html " ) ) ;
let pwd = Path ::new ( r # "C:\\Users\johan\site"# ) ;
let path = Path ::new ( r # "C:\\Users\johan\site\templates\hello.html"# ) ;
2021-03-04 18:51:33 +00:00
let config_filename = " config.toml " ;
assert_eq! ( expected , detect_change_kind ( pwd , path , config_filename ) ) ;
2018-08-04 19:47:45 +00:00
}
2018-08-05 05:59:56 +00:00
#[ test ]
fn relative_path ( ) {
let expected = ( ChangeKind ::Templates , PathBuf ::from ( " /templates/hello.html " ) ) ;
let pwd = Path ::new ( " /home/johan/site " ) ;
let path = Path ::new ( " templates/hello.html " ) ;
2021-03-04 18:51:33 +00:00
let config_filename = " config.toml " ;
assert_eq! ( expected , detect_change_kind ( pwd , path , config_filename ) ) ;
2018-08-05 05:59:56 +00:00
}
2017-03-03 08:12:40 +00:00
}