cargo_web/
deployment.rs

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
20// Note: newlines before the DOCTYPE break GitHub pages
21const 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                // TODO: Remove this eventually. We're keeping it for now
130                //       to not break compatibility with already written
131                //       `index.html` files.
132                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 ) )?; // TODO: Different error type?
249
250                    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 ) // TODO: Different error type?
263                        })?;
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 ) )?; // TODO: Different error type?
275
276                            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 ) )?; // TODO: Different error type?
282
283                        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}