1use std::collections::BTreeMap;
2use std::path::{PathBuf, Path};
3use std::io::{self, Read, Write};
4use std::fs::{self, File};
5
6use handlebars::Handlebars;
7use walkdir::WalkDir;
8use mime_guess::{Mime, guess_mime_type};
9
10use cargo_shim::{
11 TargetKind,
12 CargoPackage,
13 CargoTarget,
14 CargoResult
15};
16
17use error::Error;
18use utils::read_bytes;
19
20const DEFAULT_INDEX_HTML_TEMPLATE: &'static str = r#"<!DOCTYPE html>
22<html>
23<head>
24 <meta charset="utf-8" />
25 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
26 <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1" name="viewport" />
27 <script>
28 var Module = {};
29 var __cargo_web = {};
30 Object.defineProperty( Module, 'canvas', {
31 get: function() {
32 if( __cargo_web.canvas ) {
33 return __cargo_web.canvas;
34 }
35
36 var canvas = document.createElement( 'canvas' );
37 document.querySelector( 'body' ).appendChild( canvas );
38 __cargo_web.canvas = canvas;
39
40 return canvas;
41 }
42 });
43 </script>
44</head>
45<body>
46 <script src="{{{js_url}}}"></script>
47</body>
48</html>"#;
49
50fn generate_index_html( filename: &str ) -> String {
51 let handlebars = Handlebars::new();
52 let mut template_data = BTreeMap::new();
53 template_data.insert( "js_url", filename.to_owned() );
54 handlebars.render_template( DEFAULT_INDEX_HTML_TEMPLATE, &template_data ).unwrap()
55}
56
57enum RouteKind {
58 Blob( Vec< u8 > ),
59 StaticDirectory( PathBuf )
60}
61
62struct Route {
63 key: String,
64 kind: RouteKind,
65 can_be_deployed: bool
66}
67
68pub struct Deployment {
69 routes: Vec< Route >
70}
71
72pub enum ArtifactKind {
73 Data( Vec< u8 > ),
74 File( File )
75}
76
77pub struct Artifact {
78 pub mime_type: Mime,
79 pub kind: ArtifactKind
80}
81
82impl Artifact {
83 pub fn map_text< F: FnOnce( String ) -> String >( self, callback: F ) -> io::Result< Self > {
84 let mime_type = self.mime_type;
85 let data = match self.kind {
86 ArtifactKind::Data( data ) => data,
87 ArtifactKind::File( mut fp ) => {
88 let mut data = Vec::new();
89 fp.read_to_end( &mut data )?;
90 data
91 }
92 };
93
94 let mut text = String::from_utf8_lossy( &data ).into_owned();
95 text = callback( text );
96 let data = text.into();
97 Ok( Artifact {
98 mime_type,
99 kind: ArtifactKind::Data( data )
100 })
101 }
102}
103
104impl Deployment {
105 pub fn new( package: &CargoPackage, target: &CargoTarget, result: &CargoResult ) -> Result< Self, Error > {
106 let crate_static_path = package.crate_root.join( "static" );
107 let target_static_path = match target.kind {
108 TargetKind::Example => Some( target.source_directory.join( format!( "{}-static", target.name ) ) ),
109 TargetKind::Bin => Some( target.source_directory.join( "static" ) ),
110 _ => None
111 };
112
113 let js_name = format!( "{}.js", target.name );
114
115 let mut routes = Vec::new();
116 for path in result.artifacts() {
117 let (is_js, key) = match path.extension() {
118 Some( ext ) if ext == "js" => (true, js_name.clone()),
119 Some( ext ) if ext == "wasm" => (false, path.file_name().unwrap().to_string_lossy().into_owned()),
120 _ => continue
121 };
122
123 let contents = match read_bytes( &path ) {
124 Ok( contents ) => contents,
125 Err( error ) => return Err( Error::CannotLoadFile( path.clone(), error ) )
126 };
127
128 if is_js {
129 routes.push( Route {
133 key: "js/app.js".to_owned(),
134 kind: RouteKind::Blob( contents.clone() ),
135 can_be_deployed: false
136 });
137 }
138
139 routes.push( Route {
140 key,
141 kind: RouteKind::Blob( contents ),
142 can_be_deployed: true
143 });
144 }
145
146 if let Some( target_static_path ) = target_static_path {
147 routes.push( Route {
148 key: "".to_owned(),
149 kind: RouteKind::StaticDirectory( target_static_path.to_owned() ),
150 can_be_deployed: true
151 });
152 }
153
154 routes.push( Route {
155 key: "".to_owned(),
156 kind: RouteKind::StaticDirectory( crate_static_path.to_owned() ),
157 can_be_deployed: true
158 });
159
160 routes.push( Route {
161 key: "index.html".to_owned(),
162 kind: RouteKind::Blob( generate_index_html( &js_name ).into() ),
163 can_be_deployed: true
164 });
165
166 Ok( Deployment {
167 routes
168 })
169 }
170
171 pub fn js_url( &self ) -> &str {
172 let route = self.routes.iter().find( |route| route.can_be_deployed && route.key.ends_with( ".js" ) ).unwrap();
173 &route.key
174 }
175
176 pub fn get_by_url( &self, mut url: &str ) -> Option< Artifact > {
177 if url.starts_with( "/" ) {
178 url = &url[ 1.. ];
179 }
180
181 if url == "" {
182 url = "index.html";
183 }
184
185 let mime_type = guess_mime_type(url);
186
187 for route in &self.routes {
188 match route.kind {
189 RouteKind::Blob( ref bytes ) => {
190 if url != route.key {
191 continue;
192 }
193
194 trace!( "Get by URL of {:?}: found blob", url );
195 return Some( Artifact {
196 mime_type,
197 kind: ArtifactKind::Data( bytes.clone() )
198 });
199 },
200 RouteKind::StaticDirectory( ref path ) => {
201 let mut target_path = path.clone();
202 for chunk in url.split( "/" ) {
203 target_path = target_path.join( chunk );
204 }
205
206 trace!( "Get by URL of {:?}: path {:?} exists: {}", url, target_path, target_path.exists() );
207 if target_path.exists() {
208 match File::open( &target_path ) {
209 Ok( fp ) => {
210 return Some( Artifact {
211 mime_type,
212 kind: ArtifactKind::File( fp )
213 });
214 },
215 Err( error ) => {
216 warn!( "Cannot open {:?}: {:?}", target_path, error );
217 return None;
218 }
219 }
220 }
221 }
222 }
223 }
224
225 trace!( "Get by URL of {:?}: not found", url );
226 None
227 }
228
229 pub fn deploy_to( &self, root_directory: &Path ) -> Result< (), Error > {
230 for route in &self.routes {
231 if !route.can_be_deployed {
232 continue;
233 }
234
235 match route.kind {
236 RouteKind::Blob( ref bytes ) => {
237 let mut target_path = root_directory.to_owned();
238 for chunk in route.key.split( "/" ) {
239 target_path = target_path.join( chunk );
240 }
241
242 if target_path.exists() {
243 continue;
244 }
245
246 let target_dir = target_path.parent().unwrap();
247 fs::create_dir_all( target_dir )
248 .map_err( |err| Error::CannotCreateFile( target_dir.to_owned(), err ) )?; let mut fp = File::create( &target_path ).map_err( |err| Error::CannotCreateFile( target_path.to_owned(), err ) )?;
251 fp.write_all( &bytes ).map_err( |err| Error::CannotWriteToFile( target_path.to_owned(), err ) )?;
252 },
253 RouteKind::StaticDirectory( ref source_dir ) => {
254 if !source_dir.exists() {
255 continue;
256 }
257
258 for entry in WalkDir::new( source_dir ) {
259 let entry = entry.map_err( |err| {
260 let err_path = err.path().map( |path| path.to_owned() ).unwrap_or_else( || source_dir.clone() );
261 let err: io::Error = err.into();
262 Error::CannotLoadFile( err_path, err ) })?;
264
265 let source_path = entry.path();
266 let relative_path = source_path.strip_prefix( source_dir ).unwrap();
267 let target_path = root_directory.join( relative_path );
268 if target_path.exists() {
269 continue;
270 }
271
272 if source_path.is_dir() {
273 fs::create_dir_all( &target_path )
274 .map_err( |err| Error::CannotCreateFile( target_path.to_owned(), err ) )?; continue;
277 }
278
279 let target_dir = target_path.parent().unwrap();
280 fs::create_dir_all( target_dir )
281 .map_err( |err| Error::CannotCreateFile( target_dir.to_owned(), err ) )?; fs::copy( &source_path, &target_path )
284 .map_err( |err| Error::CannotCopyFile( source_path.to_owned(), target_path.to_owned(), err ) )?;
285 }
286 }
287 }
288 }
289
290 Ok(())
291 }
292}