diff --git a/Cargo.lock b/Cargo.lock index b5c381a..978e0ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,7 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asset-tool" -version = "0.4.10" +version = "0.4.11" dependencies = [ "anyhow", "clap", @@ -1293,7 +1293,7 @@ dependencies = [ [[package]] name = "rbx_asset" -version = "0.3.3" +version = "0.3.4" dependencies = [ "chrono", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 81b026b..f7dec71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ workspace = { members = ["rbx_asset", "rox_compiler"] } [package] name = "asset-tool" -version = "0.4.10" +version = "0.4.11" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/rbx_asset/Cargo.toml b/rbx_asset/Cargo.toml index e457342..42ee64b 100644 --- a/rbx_asset/Cargo.toml +++ b/rbx_asset/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rbx_asset" -version = "0.3.3" +version = "0.3.4" edition = "2021" publish = ["strafesnet"] repository = "https://git.itzana.me/StrafesNET/asset-tool" diff --git a/rbx_asset/src/cookie.rs b/rbx_asset/src/cookie.rs index 00b22ed..f09223d 100644 --- a/rbx_asset/src/cookie.rs +++ b/rbx_asset/src/cookie.rs @@ -31,6 +31,8 @@ pub enum CreateError{ response:String, err:std::num::ParseIntError, }, + VersionHeaderMissing, + ToStr(reqwest::header::ToStrError), } impl std::fmt::Display for CreateError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -59,6 +61,8 @@ pub enum UploadError{ response:String, err:std::num::ParseIntError, }, + VersionHeaderMissing, + ToStr(reqwest::header::ToStrError), } impl std::fmt::Display for UploadError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -70,6 +74,7 @@ impl std::error::Error for UploadError{} #[allow(nonstandard_style,dead_code)] pub struct UploadResponse{ pub AssetId:u64, + pub AssetVersion:u64, } #[allow(nonstandard_style,dead_code)] @@ -387,12 +392,23 @@ impl CookieContext{ } let response=crate::response_ok( self.post(url,body).await.map_err(CreateError::PostError)? - ).await.map_err(CreateError::Response)? - .text().await.map_err(CreateError::Reqwest)?; + ).await.map_err(CreateError::Response)?; + + let version_str=response + .headers() + .get("roblox-assetversionnumber") + .ok_or(CreateError::VersionHeaderMissing)? + .to_str() + .map_err(CreateError::ToStr)?; + let version=version_str.parse() + .map_err(|err|CreateError::ParseInt{err,response:version_str.to_owned()})?; + + let response=response.text().await.map_err(CreateError::Reqwest)?; match response.parse(){ Ok(asset_id)=>Ok(UploadResponse{ AssetId:asset_id, + AssetVersion:version, }), Err(err)=>Err(CreateError::ParseInt{ response, @@ -428,12 +444,23 @@ impl CookieContext{ } let response=crate::response_ok( self.post(url,body).await.map_err(UploadError::PostError)? - ).await.map_err(UploadError::Response)? - .text().await.map_err(UploadError::Reqwest)?; + ).await.map_err(UploadError::Response)?; + + let version_str=response + .headers() + .get("roblox-assetversionnumber") + .ok_or(UploadError::VersionHeaderMissing)? + .to_str() + .map_err(UploadError::ToStr)?; + let version=version_str.parse() + .map_err(|err|UploadError::ParseInt{err,response:version_str.to_owned()})?; + + let response=response.text().await.map_err(UploadError::Reqwest)?; match response.parse(){ Ok(asset_id)=>Ok(UploadResponse{ AssetId:asset_id, + AssetVersion:version, }), Err(err)=>Err(UploadError::ParseInt{ response, diff --git a/src/main.rs b/src/main.rs index e8501ba..9d4c79b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,6 +133,8 @@ struct DownloadUserInventoryJsonSubcommand{ output_folder:Option<PathBuf>, #[arg(long)] user_id:u64, + #[arg(long)] + continue_from_cursor:Option<bool>, } /// Upload a (.rbxm, .rbxmx) model file, creating a new asset. Can be any type of model, including modulescripts. #[derive(Args)] @@ -539,6 +541,7 @@ async fn main()->AResult<()>{ ).await?, subcommand.user_id, subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()), + subcommand.continue_from_cursor.unwrap_or(false), ).await, Commands::CreateAsset(subcommand)=>create_asset(CreateAssetConfig{ cookie:cookie_from_args( @@ -1058,30 +1061,70 @@ async fn download_creations_json(cookie:Cookie,owner:rbx_asset::cookie::Owner,ou Ok(()) } -async fn get_user_inventory_pages(context:&CookieContext,user_id:u64)->AResult<Vec<rbx_asset::cookie::UserInventoryItem>>{ - let mut config=rbx_asset::cookie::UserInventoryPageRequest{ - user_id, - cursor:None, - }; - let mut asset_list=Vec::new(); +async fn get_user_inventory_pages( + context:&CookieContext, + asset_list:&mut Vec<rbx_asset::cookie::UserInventoryItem>, + config:&mut rbx_asset::cookie::UserInventoryPageRequest, +)->AResult<()>{ loop{ - let mut page=context.get_user_inventory_page(&config).await?; - asset_list.append(&mut page.data); - if page.nextPageCursor.is_none(){ + let page=context.get_user_inventory_page(&config).await?; + asset_list.extend(page.data); + config.cursor=page.nextPageCursor; + if config.cursor.is_none(){ break; } - config.cursor=page.nextPageCursor; } - Ok(asset_list) + Ok(()) } -async fn download_user_inventory_json(cookie:Cookie,user_id:u64,output_folder:PathBuf)->AResult<()>{ - let context=CookieContext::new(cookie); - let item_list=get_user_inventory_pages(&context,user_id).await?; +async fn download_user_inventory_json(cookie:Cookie,user_id:u64,output_folder:PathBuf,continue_from_cursor:bool)->AResult<()>{ + let mut versions_path=output_folder.clone(); + versions_path.set_file_name("versions.json"); + let mut cursor_path=output_folder.clone(); + cursor_path.set_file_name("cursor.json"); - let mut path=output_folder.clone(); - path.set_file_name("versions.json"); - tokio::fs::write(path,serde_json::to_string(&item_list)?).await?; + let context=CookieContext::new(cookie); + + let (mut asset_list,mut config)=if continue_from_cursor{ + // load state from files + let (versions,cursor)=tokio::try_join!( + tokio::fs::read(versions_path.as_path()), + tokio::fs::read_to_string(cursor_path.as_path()), + )?; + ( + serde_json::from_slice(&versions)?, + rbx_asset::cookie::UserInventoryPageRequest{ + user_id, + cursor:Some(cursor), + } + ) + }else{ + // create new state + ( + Vec::new(), + rbx_asset::cookie::UserInventoryPageRequest{ + user_id, + cursor:None, + } + ) + }; + + match get_user_inventory_pages(&context,&mut asset_list,&mut config).await{ + Ok(())=>println!("Pages polling complete"), + Err(e)=>println!("Error: {e}"), + } + + let cursor_fut=async{ + if let Some(cursor)=config.cursor{ + println!("writing cursor state..."); + // there was a problem, write out cursor + tokio::fs::write(cursor_path,cursor).await?; + } + Ok(()) + }; + let versions_fut=tokio::fs::write(versions_path,serde_json::to_string(&asset_list)?); + + tokio::try_join!(versions_fut,cursor_fut)?; Ok(()) }