use std::io;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::fs::File;
use std::fs;
use std::str;
use std::default::Default;
use std::env;
use std::error::Error as StdError;
use std::process;
use toml;
#[macro_use] extern crate clap;
use clap::App;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json;
#[macro_use] extern crate failure;
use failure::Error;
use failure::ResultExt;
use rusoto_core::{Region, ByteStream};
use rusoto_core::request::HttpClient;
use rusoto_credential::StaticProvider;
use rusoto_cognito_identity::*;
use rusoto_cognito_idp::*;
use rusoto_s3::*;
mod prompt;
mod account;
mod config;
mod template;
mod package;
mod cache;
use config::*;
use template::load_templates;
use package::wasm_package;
use cache::FileCache;
enum Command {
Init,
NewProject,
Setup,
Deploy,
Update,
Unknown,
}
impl From<&str> for Command {
fn from(s: &str) -> Command {
match s {
"init" => Command::Init,
"new" => Command::NewProject,
"setup" => Command::Setup,
"deploy" => Command::Deploy,
"update" => Command::Update,
_ => Command::Unknown
}
}
}
fn ensure_refresh_token(cache: &FileCache, client: &CognitoIdentityProviderClient) -> String {
cache.get_encrypted("refresh_token")
.or_else::<io::Error, _>(|_| {
let creds = prompt::login();
let token = account::login(&client, creds.username, creds.password).sync()
.and_then(|resp| {
let token = resp
.authentication_result.expect("Failed")
.refresh_token.expect("Missing refresh token");
cache.set_encrypted("refresh_token", token.as_bytes().to_vec())
.expect("Failed to cache refresh token");
Ok(token)})
.or_else::<io::Error, _>(|_| {
Ok(ensure_refresh_token(cache, client))
})
.expect("Something went wrong");
Ok(token)
})
.unwrap()
}
fn ensure_identity_id(cache: &FileCache, client: &CognitoIdentityClient, id_token: &str)
-> String {
cache.get("identity")
.or_else::<io::Error, _>(|_| {
let id = account::identity_id(client, id_token)
.sync()
.expect("Failed to get identity ID")
.identity_id.expect("No identity ID");
cache.set("identity", id.as_bytes().to_vec())
.expect("Failed to add identity to cache");
Ok(id)
})
.unwrap()
}
fn run() -> Result<(), Error> {
let handlebars = load_templates().context("Failed to load templates")?;
let yaml = load_yaml!("cli.yaml");
let app = App::from_yaml(yaml);
let input = app.get_matches();
let project_path = input.args.get("project")
.map_or(env::current_dir(),
|arg| Ok(PathBuf::from(&arg.vals[0])))
.context("Failed to get project path")?;
println!("Using project path {}", project_path.to_str().unwrap());
let home_path = input.args.get("home")
.map_or(default_home_path(),
|arg| Ok(PathBuf::from(&arg.vals[0])))
.context("Failed to get woz home path")?;
let encryption_key = FileCache::make_key(ENCRYPTION_PASSWORD, ENCRYPTION_SALT);
let cache = FileCache::new(encryption_key, home_path.clone());
println!("Using home path {}", home_path.to_str().unwrap());
if let Some(sub) = input.subcommand_name() {
match Command::from(sub) {
Command::Setup => {
fs::create_dir_all(&home_path).context("Failed to make home directory")?;
let values = prompt::signup();
let client = CognitoIdentityProviderClient::new(Region::UsWest2);
account::signup(client, values.email, values.username, values.password)
.sync()
.and_then(|resp| {
let user_id = resp.user_sub;
cache.set("user", user_id.as_bytes().to_vec())
.expect("Failed add user ID to cache");
Ok(())
})
.or_else(|e| {
println!("Signup failed {}", e.description());
Err(e)
})?;
println!("Please check your inbox for an email to verify your account.");
},
Command::NewProject => {
let subcommand_args = input.subcommand_matches("new").unwrap();
let project_name = subcommand_args.value_of("NAME").unwrap();
let command = format!("cargo new {} --lib", project_name);
process::Command::new("sh")
.arg("-c")
.arg(command)
.output()
.context("Failed to create new project using cargo")?;
let mut cargo_conf = fs::OpenOptions::new()
.append(true)
.open(PathBuf::from(format!("{}/Cargo.toml", project_name)))
.context("Failed to open Cargo.toml")?;
cargo_conf.write_all("seed = \"0.2.9\"
wasm-bindgen = \"0.2.40\"
web-sys = \"0.3.14\"
[lib]
crate-type = [\"cdylib\"]".as_bytes()).unwrap();
let mut woz_conf = File::create(
PathBuf::from(format!("{}/woz.toml", project_name))
).context("Failed to create woz config")?;
woz_conf.write_all(format!("name=\"Example: My App\"
project_id=\"{}\"
short_name=\"MyApp\"
lib=\"wasm-bindgen\"
wasm_path=\"target/wasm32-unknown-unknown/release/{}.wasm\"
", project_name, project_name).as_bytes()).unwrap();
let mut default_lib_rs = File::create(
PathBuf::from(format!("{}/src/lib.rs", project_name))
).context("Failed to create lib.rs")?;
default_lib_rs.write_all(DEFAULT_PROJECT_LIB_RS.as_bytes()).unwrap();
println!("New project created! Please cd to ./{}", project_name);
},
Command::Init => {
println!("Initializing current project directory...");
let cargo_conf = fs::read_to_string("./Config.toml")
.context("You must be in a cargo project")?
.parse::<toml::Value>()
.context("Failed to read Cargo.toml")?;
let project_name = &cargo_conf["package"]["name"];
let mut woz_conf = File::create(
PathBuf::from(format!("{}/woz.toml", project_name))
).context("Failed to create woz config")?;
woz_conf.write_all(format!("name=\"{}\"
project_id=\"{}\"
short_name=\"{}\"
lib=\"wasm-bindgen\"
wasm_path=\"target/wasm32-unknown-unknown/release/{}.wasm\"
", project_name, project_name, project_name, project_name).as_bytes()).unwrap();
println!("Ready to be deployed with 'woz deploy'");
},
Command::Deploy => {
println!("Deploying...");
let mut build_proc = process::Command::new("sh")
.arg("-c")
.arg("cargo build --release --target wasm32-unknown-unknown")
.stdout(process::Stdio::piped())
.spawn()
.context("Failed to spawn build")?;
let exit_code = build_proc.wait().context("Failed to wait for build")?;
if !exit_code.success() {
return Err(format_err!("Build failed, please check output above."))
}
let mut conf_path = project_path.clone();
conf_path.push("woz.toml");
let conf_str = fs::read_to_string(conf_path.clone())
.context(format!("Couldn't find woz config file at {}",
conf_path.clone().to_str().unwrap()))?;
let conf: Config = toml::from_str(&conf_str).context("Failed to parse woz config")?;
let ProjectId(project_id) = conf.project_id;
let id_provider_client = CognitoIdentityProviderClient::new(Region::UsWest2);
let id_client = CognitoIdentityClient::new(Region::UsWest2);
let refresh_token = ensure_refresh_token(&cache, &id_provider_client);
let id_token = account::refresh_auth(&id_provider_client, &refresh_token)
.sync()
.or_else(|err| {
println!("Getting refresh token failed: {}", err);
let creds = prompt::login();
account::login(&id_provider_client, creds.username, creds.password)
.sync()
.or_else(|e| {
println!("Login failed: {}", e);
Err(e)
})
.and_then(|resp| {
let token = resp.clone()
.authentication_result.expect("Failed")
.refresh_token.expect("Missing refresh token");
cache.set_encrypted(
"refresh_token",
token.as_bytes().to_vec()
).expect("Failed to cache refresh token");
Ok(resp)})
})
.context("Failed to get id token")?
.authentication_result.expect("No auth result")
.id_token.expect("No access token");
let identity_id = ensure_identity_id(&cache, &id_client, &id_token);
let aws_creds = account::aws_credentials(&id_client, &identity_id, &id_token)
.sync()
.context("Failed to fetch AWS credentials")?
.credentials.expect("Missing credentials");
let creds_provider = StaticProvider::new(
aws_creds.access_key_id.expect("Missing access key"),
aws_creds.secret_key.expect("Missing secret key"),
Some(aws_creds.session_token.expect("Missing session token")),
Some(aws_creds.expiration.expect("Missing expiration") as i64)
);
let request_dispatcher = HttpClient::new();
let s3_client = S3Client::new_with(
request_dispatcher.context("Failed to make an HttpClient")?,
creds_provider,
Region::UsWest2
);
let mut out_path = home_path.clone();
out_path.push(&project_id);
out_path.push("pkg");
fs::create_dir_all(&out_path).context("Failed to make pkg directory")?;
let mut wasm_path = project_path.clone();
wasm_path.push(conf.wasm_path);
let index_template = handlebars.render("index", &json!({
"name": conf.name,
"author": conf.author,
"description": conf.description,
"manifest_path": "./manifest.json",
"app_js_path": "./app.js",
"sw_js_path": "./sw.js",
"wasm_path": "./app.wasm",
}));
let manifest_template = handlebars.render("manifest", &json!({
"name": conf.name,
"short_name": conf.short_name,
"bg_color": "#ffffff",
"description": conf.description
}));
let service_worker_template = handlebars.render("sw.js", &json!({}));
let wasm_package = wasm_package(
conf.lib.unwrap(),
wasm_path,
out_path,
).context("Failed to generate wasm package")?;
let key_prefix = format!("{}/{}", &identity_id, &project_id);
let mut files = vec![
(format!("{}/index.html", key_prefix),
String::from("text/html"),
index_template.context("Failed to render index.html")?.into_bytes()),
(format!("{}/manifest.json", key_prefix),
String::from("application/manifest+json"),
manifest_template.context("Failed to render manifest.json")?.into_bytes()),
(format!("{}/sw.js", key_prefix),
String::from("application/javascript"),
service_worker_template.context("Failed to render sw.js")?.into_bytes()),
(format!("{}/app.js", key_prefix),
String::from("application/javascript"),
fs::read_to_string(wasm_package.js).context("Failed to read js file")?.into_bytes()),
(format!("{}/app.wasm", key_prefix),
String::from("application/wasm"),
{
let mut f = File::open(wasm_package.wasm).context("Failed to read wasm file")?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer).context("Failed to read to bytes")?;
buffer
}),
];
if let Some(icons) = conf.icons {
for (size, path) in icons.to_vec() {
let mut f = File::open(path)
.context("Icon file does not exist")?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)
.context("Failed to read icon to bytes")?;
files.push(
(format!("{}/img/icons/homescreen_{}.png", key_prefix, size),
String::from("image/png"),
buffer)
)
}
} else {
for (size, bytes) in DEFAULT_ICONS.iter() {
files.push(
(format!("{}/img/icons/homescreen_{}.png", key_prefix, size),
String::from("image/png"),
bytes.to_owned())
);
};
}
if let Some(splashscreens) = conf.splashscreens {
for (device, path) in splashscreens.to_vec() {
let mut f = File::open(path)
.context("Splashscreen file does not exist")?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)
.context("Failed to read splashscreen to bytes")?;
files.push(
(format!("{}/img/splashscreens/{}.png", key_prefix, device),
String::from("image/png"),
buffer)
)
};
} else {
for (device, bytes) in DEFAULT_SPLASHSCREENS.iter() {
files.push(
(format!("{}/img/splashscreens/{}.png", key_prefix, device),
String::from("image/png"),
bytes.to_owned())
);
};
}
for (file_name, mimetype, body) in files.into_iter() {
let req = PutObjectRequest {
bucket: String::from(S3_BUCKET_NAME),
key: file_name.clone(),
body: Some(ByteStream::from(body)),
content_type: Some(mimetype),
..Default::default()
};
s3_client.put_object(req)
.sync()
.context(format!("Failed to upload file to S3: {}", file_name))?;
};
let location = format!(
"{}://{}/{}/{}/index.html",
SCHEME,
NETLOC,
identity_id,
project_id
);
println!("{}", format!("Your app is available at {}", location));
}
_ => unimplemented!()
};
};
Ok(())
}
fn main() {
run().map_err(|e| println!("{}", e)).ok();
}