Skip to content

Commit

Permalink
Split CLI into subcommands (#47)
Browse files Browse the repository at this point in the history
* Initial commit

* Add list command
  • Loading branch information
jackTabsCode authored May 26, 2024
1 parent f232844 commit f93f34b
Show file tree
Hide file tree
Showing 16 changed files with 292 additions and 251 deletions.
10 changes: 0 additions & 10 deletions src/args.rs

This file was deleted.

25 changes: 25 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use clap::{Args, Parser, Subcommand};

#[derive(Parser)]
#[command(version, about = "Upload and reference Roblox assets in code.")]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
/// Sync assets to Roblox.
Sync(SyncArgs),

/// List assets in the lockfile.
List,
}

#[derive(Args)]
pub struct SyncArgs {
/// Your Open Cloud API key.
/// Can also be set with the ASPHALT_API_KEY environment variable.
#[arg(short, long)]
pub api_key: Option<String>,
}
9 changes: 9 additions & 0 deletions src/commands/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::LockFile;

pub async fn list(lockfile: LockFile) -> anyhow::Result<()> {
for (path, entry) in lockfile.entries {
println!("\"{}\": {}", path, entry.asset_id);
}

Ok(())
}
2 changes: 2 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod list;
pub mod sync;
4 changes: 2 additions & 2 deletions src/codegen/flat.rs → src/commands/sync/codegen/flat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn generate_lua(
.entries
.iter()
.map(|(file_path, file_entry)| {
let file_stem = asset_path(&file_path, &strip_dir, strip_extension)?;
let file_stem = asset_path(file_path, strip_dir, strip_extension)?;
Ok(format!(
"\t[\"{}\"] = \"rbxassetid://{}\"",
file_stem, file_entry.asset_id
Expand All @@ -46,7 +46,7 @@ pub fn generate_ts(
.entries
.keys()
.map(|file_path| {
let file_stem = asset_path(&file_path, &strip_dir, strip_extension)?;
let file_stem = asset_path(file_path, strip_dir, strip_extension)?;
Ok(format!("\t\"{}\": string", file_stem))
})
.collect::<Result<Vec<String>, anyhow::Error>>()?
Expand Down
3 changes: 1 addition & 2 deletions src/codegen/mod.rs → src/commands/sync/codegen/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::{config::StyleType, LockFile};

use crate::{commands::sync::config::StyleType, LockFile};
mod flat;
mod nested;

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
mod ast;

use self::types::NestedTable;
use crate::LockFile;
use anyhow::{bail, Context};
use ast::{AstTarget, Expression, ReturnStatement};
use std::collections::BTreeMap;
use std::fmt::Write;
use std::path::PathBuf;
use std::{path::Component as PathComponent, path::Path};

use crate::LockFile;
use ast::{AstTarget, Expression, ReturnStatement};
use std::fmt::Write;

use self::types::NestedTable;
mod ast;

pub(crate) mod types {
use std::collections::BTreeMap;
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs → src/commands/sync/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub struct CodegenConfig {
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub struct SyncConfig {
pub asset_dir: String,
pub write_dir: String,
pub creator: Creator,
Expand Down
221 changes: 221 additions & 0 deletions src/commands/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use self::state::SyncState;
use crate::{cli::SyncArgs, FileEntry, LockFile};
use anyhow::{anyhow, Context};
use blake3::Hasher;
use codegen::{generate_lua, generate_ts};
use config::SyncConfig;
use console::style;
use image::{DynamicImage, ImageFormat};
use rbxcloud::rbx::v1::assets::AssetType;
use std::{collections::VecDeque, io::Cursor, path::Path};
use tokio::fs::{read, read_dir, read_to_string, write, DirEntry};
use upload::upload_asset;
use util::{alpha_bleed::alpha_bleed, svg::svg_to_png};

mod codegen;
pub mod config;
mod state;
mod upload;
mod util;

fn fix_path(path: &str) -> String {
path.replace('\\', "/")
}

async fn check_file(entry: &DirEntry, state: &SyncState) -> anyhow::Result<Option<FileEntry>> {
let path = entry.path();
let path_str = path.to_str().context("Failed to convert path to string")?;
let fixed_path = fix_path(path_str);

let mut bytes = read(&path)
.await
.with_context(|| format!("Failed to read {}", fixed_path))?;

let mut extension = match path.extension().and_then(|s| s.to_str()) {
Some(extension) => extension,
None => return Ok(None),
};

if extension == "svg" {
bytes = svg_to_png(&bytes, &state.font_db)
.await
.with_context(|| format!("Failed to convert SVG to PNG: {}", fixed_path))?;
extension = "png";
}

let asset_type = match AssetType::try_from_extension(extension) {
Ok(asset_type) => asset_type,
Err(e) => {
eprintln!(
"Skipping {} because it has an unsupported extension: {}",
style(fixed_path).yellow(),
e
);
return Ok(None);
}
};

#[cfg(feature = "alpha_bleed")]
match asset_type {
AssetType::DecalJpeg | AssetType::DecalBmp | AssetType::DecalPng => {
let mut image: DynamicImage = image::load_from_memory(&bytes)?;
alpha_bleed(&mut image);

let format = ImageFormat::from_extension(extension).ok_or(anyhow!(
"Failed to get image format from extension: {}",
extension
))?;

let mut new_bytes: Cursor<Vec<u8>> = Cursor::new(Vec::new());
image.write_to(&mut new_bytes, format)?;

bytes = new_bytes.into_inner();
}
_ => {}
}

let mut hasher = Hasher::new();
hasher.update(&bytes);
let hash = hasher.finalize().to_string();

let existing = state.existing_lockfile.entries.get(fixed_path.as_str());

if let Some(existing_value) = existing {
if existing_value.hash == hash {
return Ok(Some(FileEntry {
hash,
asset_id: existing_value.asset_id,
}));
}
}

let file_name = path
.file_name()
.with_context(|| format!("Failed to get file name of {}", fixed_path))?
.to_str()
.with_context(|| format!("Failed to convert file name to string: {}", fixed_path))?;

let asset_id = upload_asset(
bytes,
file_name,
asset_type,
state.api_key.clone(),
state.creator.clone(),
)
.await
.with_context(|| format!("Failed to upload {}", fixed_path))?;

eprintln!("Uploaded {}", style(fixed_path).green());

Ok(Some(FileEntry { hash, asset_id }))
}

pub async fn sync(args: SyncArgs, existing_lockfile: LockFile) -> anyhow::Result<()> {
let config: SyncConfig = {
let file_contents = read_to_string("asphalt.toml")
.await
.context("Failed to read asphalt.toml")?;
toml::from_str(&file_contents).context("Failed to parse config")
}?;

let mut state = SyncState::new(args, config, existing_lockfile)
.await
.context("Failed to create state")?;

eprintln!("{}", style("Syncing...").dim());

let mut remaining_items = VecDeque::new();
remaining_items.push_back(state.asset_dir.clone());

while let Some(path) = remaining_items.pop_front() {
let mut dir_entries = read_dir(path.clone()).await.with_context(|| {
format!(
"Failed to read directory: {}",
path.to_str().unwrap_or("???")
)
})?;

while let Some(entry) = dir_entries
.next_entry()
.await
.with_context(|| format!("Failed to read directory entry: {:?}", path))?
{
let entry_path = entry.path();
if entry_path.is_dir() {
remaining_items.push_back(entry_path);
} else {
let result = match check_file(&entry, &state).await {
Ok(Some(result)) => result,
Ok(None) => continue,
Err(e) => {
eprintln!("{} {:?}", style("Error:").red(), e);
continue;
}
};

let path_str = entry_path.to_str().with_context(|| {
format!("Failed to convert path to string: {:?}", entry_path)
})?;
let fixed_path = fix_path(path_str);

state.new_lockfile.entries.insert(fixed_path, result);
}
}
}

write(
"asphalt.lock.toml",
toml::to_string(&state.new_lockfile).context("Failed to serialize lockfile")?,
)
.await
.context("Failed to write lockfile")?;

let asset_dir_str = state
.asset_dir
.to_str()
.context("Failed to convert asset directory to string")?;

state
.new_lockfile
.entries
.extend(state.existing.into_iter().map(|(path, asset)| {
(
path,
FileEntry {
hash: "".to_string(),
asset_id: asset.id,
},
)
}));

let lua_filename = format!("{}.{}", state.output_name, state.lua_extension);
let lua_output = generate_lua(
&state.new_lockfile,
asset_dir_str,
&state.style,
state.strip_extension,
);

write(Path::new(&state.write_dir).join(lua_filename), lua_output?)
.await
.context("Failed to write output Lua file")?;

if state.typescript {
let ts_filename = format!("{}.d.ts", state.output_name);
let ts_output = generate_ts(
&state.new_lockfile,
asset_dir_str,
state.output_name.as_str(),
&state.style,
state.strip_extension,
);

write(Path::new(&state.write_dir).join(ts_filename), ts_output?)
.await
.context("Failed to write output TypeScript file")?;
}

eprintln!("{}", style("Synced!").dim());

Ok(())
}
26 changes: 13 additions & 13 deletions src/state.rs → src/commands/sync/state.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use crate::{
args::Args,
config::{Config, CreatorType, ExistingAsset, StyleType},
cli::SyncArgs,
commands::sync::config::{CreatorType, ExistingAsset, StyleType},
LockFile,
};
use anyhow::Context;
use rbxcloud::rbx::v1::assets::{AssetCreator, AssetGroupCreator, AssetUserCreator};
use resvg::usvg::fontdb::Database;
use std::{collections::HashMap, env, path::PathBuf};
use tokio::fs::{create_dir_all, read_to_string};
use tokio::fs::create_dir_all;

use super::config::SyncConfig;

fn add_trailing_slash(path: &str) -> String {
if !path.ends_with('/') {
Expand All @@ -25,7 +27,8 @@ fn get_api_key(arg_key: Option<String>) -> anyhow::Result<String> {
None => env_key.context("No API key provided"),
}
}
pub struct State {

pub struct SyncState {
pub asset_dir: PathBuf,
pub write_dir: PathBuf,

Expand All @@ -46,8 +49,12 @@ pub struct State {
pub existing: HashMap<String, ExistingAsset>,
}

impl State {
pub async fn new(args: Args, config: Config) -> anyhow::Result<Self> {
impl SyncState {
pub async fn new(
args: SyncArgs,
config: SyncConfig,
existing_lockfile: LockFile,
) -> anyhow::Result<Self> {
let api_key = get_api_key(args.api_key)?;

let creator: AssetCreator = match config.creator.creator_type {
Expand Down Expand Up @@ -89,13 +96,6 @@ impl State {
let mut font_db = Database::new();
font_db.load_system_fonts();

let existing_lockfile: LockFile = toml::from_str(
&read_to_string("asphalt.lock.toml")
.await
.unwrap_or_default(),
)
.unwrap_or_default();

let new_lockfile: LockFile = Default::default();

let manual = config.existing.unwrap_or_default();
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit f93f34b

Please sign in to comment.