mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
bonsaiviewer-autodesk: satisfy cargo fmt and clippy
The crate had never been run through rustfmt, so the CI job's first step (`cargo fmt --all -- --check`) failed and masked 12 clippy errors behind it. Fix both. Beyond the mechanical reformat and the redundant-closure/div_ceil/Default lints, two changes carry meaning: - SettingsDialog::on_reload is UI-thread only, so it becomes an Rc. The Arc was never shared across threads and clippy rightly flagged it as an Arc over a non-Send/Sync closure. - WorkerMsg variants lose their shared `Loaded` postfix; the enum's doc comment already says these are worker completions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,7 +77,11 @@ impl ApsClient {
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().map(entry_from_jsonapi).collect())
|
||||
.unwrap_or_default();
|
||||
folders.sort_by(|a, b| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()));
|
||||
folders.sort_by(|a, b| {
|
||||
a.display_name
|
||||
.to_lowercase()
|
||||
.cmp(&b.display_name.to_lowercase())
|
||||
});
|
||||
Ok(folders)
|
||||
}
|
||||
|
||||
@@ -120,7 +124,11 @@ impl ApsClient {
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
entries.sort_by(|a, b| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()));
|
||||
entries.sort_by(|a, b| {
|
||||
a.display_name
|
||||
.to_lowercase()
|
||||
.cmp(&b.display_name.to_lowercase())
|
||||
});
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
@@ -136,10 +144,17 @@ impl ApsClient {
|
||||
.get("data")
|
||||
.cloned()
|
||||
.ok_or_else(|| RpcError::internal("Item response missing 'data'."))?;
|
||||
let item_attrs = item.get("attributes").cloned().unwrap_or_else(|| serde_json::json!({}));
|
||||
let item_attrs = item
|
||||
.get("attributes")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
let parent_folder_id = relationship_id(&item, "parent");
|
||||
let tip_id = relationship_id(&item, "tip");
|
||||
let id = item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string();
|
||||
let id = item
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let tip = payload
|
||||
.get("included")
|
||||
.and_then(|v| v.as_array())
|
||||
@@ -171,7 +186,10 @@ impl ApsClient {
|
||||
parent_folder_id,
|
||||
});
|
||||
};
|
||||
let tip_attrs = tip.get("attributes").cloned().unwrap_or_else(|| serde_json::json!({}));
|
||||
let tip_attrs = tip
|
||||
.get("attributes")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
let display = tip_attrs
|
||||
.get("displayName")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -182,12 +200,21 @@ impl ApsClient {
|
||||
Ok(ItemTip {
|
||||
id,
|
||||
display_name: display,
|
||||
hidden: item_attrs.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
hidden: item_attrs
|
||||
.get("hidden")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
version_id: tip.get("id").and_then(|v| v.as_str()).map(String::from),
|
||||
storage_id: relationship_id(&tip, "storage"),
|
||||
version_number: tip_attrs.get("versionNumber").cloned(),
|
||||
last_modified_time_utc: tip_attrs.get("lastModifiedTime").and_then(|v| v.as_str()).map(String::from),
|
||||
last_modified_user_name: tip_attrs.get("lastModifiedUserName").and_then(|v| v.as_str()).map(String::from),
|
||||
last_modified_time_utc: tip_attrs
|
||||
.get("lastModifiedTime")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
last_modified_user_name: tip_attrs
|
||||
.get("lastModifiedUserName")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
parent_folder_id,
|
||||
})
|
||||
}
|
||||
@@ -195,8 +222,16 @@ impl ApsClient {
|
||||
|
||||
fn hub_from_jsonapi(item: &Value) -> Hub {
|
||||
Hub {
|
||||
id: item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
|
||||
name: item.pointer("/attributes/name").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
|
||||
id: item
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
name: item
|
||||
.pointer("/attributes/name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
extension_type: item
|
||||
.pointer("/attributes/extension/type")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -207,8 +242,16 @@ fn hub_from_jsonapi(item: &Value) -> Hub {
|
||||
|
||||
fn project_from_jsonapi(item: &Value) -> Project {
|
||||
Project {
|
||||
id: item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
|
||||
name: item.pointer("/attributes/name").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
|
||||
id: item
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
name: item
|
||||
.pointer("/attributes/name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
extension_type: item
|
||||
.pointer("/attributes/extension/type")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -223,7 +266,10 @@ fn project_from_jsonapi(item: &Value) -> Project {
|
||||
}
|
||||
|
||||
pub(crate) fn entry_from_jsonapi(item: &Value) -> Entry {
|
||||
let attrs = item.get("attributes").cloned().unwrap_or_else(|| serde_json::json!({}));
|
||||
let attrs = item
|
||||
.get("attributes")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
let display = attrs
|
||||
.get("displayName")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -237,7 +283,11 @@ pub(crate) fn entry_from_jsonapi(item: &Value) -> Entry {
|
||||
_ => EntryType::Other,
|
||||
};
|
||||
Entry {
|
||||
id: item.get("id").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
|
||||
id: item
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
entry_type,
|
||||
display_name: display,
|
||||
name: attrs.get("name").and_then(|v| v.as_str()).map(String::from),
|
||||
|
||||
@@ -23,7 +23,9 @@ impl ApsClient {
|
||||
pub fn with_base_url(auth: Arc<AuthSessionService>, base_url: String) -> Self {
|
||||
Self {
|
||||
auth,
|
||||
agent: ureq::AgentBuilder::new().timeout(Duration::from_secs(120)).build(),
|
||||
agent: ureq::AgentBuilder::new()
|
||||
.timeout(Duration::from_secs(120))
|
||||
.build(),
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,13 @@ impl ApsClient {
|
||||
local_path.display()
|
||||
)));
|
||||
}
|
||||
let file_name = display_name
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| local_path.file_name().unwrap_or_default().to_string_lossy().into_owned());
|
||||
let file_name = display_name.map(str::to_string).unwrap_or_else(|| {
|
||||
local_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
});
|
||||
let storage_id = self.create_storage(project_id, folder_id, &file_name)?;
|
||||
let (bucket, object) = parse_storage_id(&storage_id)?;
|
||||
self.upload_local_file_to_oss(&bucket, &object, local_path, progress)?;
|
||||
@@ -68,7 +72,9 @@ impl ApsClient {
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from)
|
||||
.ok_or_else(|| RpcError::internal("Signed download URL response did not contain a URL."))
|
||||
.ok_or_else(|| {
|
||||
RpcError::internal("Signed download URL response did not contain a URL.")
|
||||
})
|
||||
}
|
||||
|
||||
fn download_to_file(
|
||||
@@ -81,7 +87,11 @@ impl ApsClient {
|
||||
Ok(r) => r,
|
||||
Err(ureq::Error::Status(code, resp)) => {
|
||||
let body = resp.into_string().unwrap_or_default();
|
||||
let msg = if body.trim().is_empty() { format!("HTTP {code}") } else { body.trim().to_string() };
|
||||
let msg = if body.trim().is_empty() {
|
||||
format!("HTTP {code}")
|
||||
} else {
|
||||
body.trim().to_string()
|
||||
};
|
||||
return Err(RpcError::internal(msg));
|
||||
}
|
||||
Err(e) => return Err(RpcError::internal(e.to_string())),
|
||||
@@ -93,8 +103,9 @@ impl ApsClient {
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let mut reader = resp.into_reader();
|
||||
let mut file = File::create(destination)
|
||||
.map_err(|e| RpcError::internal(format!("Cannot create '{}': {e}", destination.display())))?;
|
||||
let mut file = File::create(destination).map_err(|e| {
|
||||
RpcError::internal(format!("Cannot create '{}': {e}", destination.display()))
|
||||
})?;
|
||||
let mut buf = vec![0u8; READ_BUF];
|
||||
let mut downloaded: u64 = 0;
|
||||
loop {
|
||||
@@ -108,16 +119,26 @@ impl ApsClient {
|
||||
.map_err(|e| RpcError::internal(format!("Download write failed: {e}")))?;
|
||||
downloaded += n as u64;
|
||||
if let Some(cb) = &progress {
|
||||
let percent = total_bytes.map(|t| min(100, ((downloaded as f64 / t as f64) * 100.0) as i32));
|
||||
let percent =
|
||||
total_bytes.map(|t| min(100, ((downloaded as f64 / t as f64) * 100.0) as i32));
|
||||
cb(&name, percent, Some(downloaded), total_bytes);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_storage(&self, project_id: &str, folder_id: &str, file_name: &str) -> Result<String, RpcError> {
|
||||
fn create_storage(
|
||||
&self,
|
||||
project_id: &str,
|
||||
folder_id: &str,
|
||||
file_name: &str,
|
||||
) -> Result<String, RpcError> {
|
||||
let payload = self.post_json(
|
||||
&format!("{}/data/v1/projects/{}/storage", self.base_url, url_enc(project_id)),
|
||||
&format!(
|
||||
"{}/data/v1/projects/{}/storage",
|
||||
self.base_url,
|
||||
url_enc(project_id)
|
||||
),
|
||||
&json!({
|
||||
"jsonapi": {"version": "1.0"},
|
||||
"data": {
|
||||
@@ -145,7 +166,7 @@ impl ApsClient {
|
||||
let file_size = std::fs::metadata(local_path)
|
||||
.map_err(|e| RpcError::internal(format!("stat '{}': {e}", local_path.display())))?
|
||||
.len();
|
||||
let total_parts = ((file_size + CHUNK_SIZE - 1) / CHUNK_SIZE).max(1);
|
||||
let total_parts = file_size.div_ceil(CHUNK_SIZE).max(1);
|
||||
let file_name = local_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
@@ -170,13 +191,18 @@ impl ApsClient {
|
||||
parts_to_request,
|
||||
)?;
|
||||
if upload_key.is_none() {
|
||||
upload_key = signed.get("uploadKey").and_then(|v| v.as_str()).map(String::from);
|
||||
upload_key = signed
|
||||
.get("uploadKey")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
}
|
||||
let urls = signed
|
||||
.get("urls")
|
||||
.and_then(|v| v.as_array())
|
||||
.filter(|a| !a.is_empty())
|
||||
.ok_or_else(|| RpcError::internal("Upload URL response did not contain upload URLs."))?
|
||||
.ok_or_else(|| {
|
||||
RpcError::internal("Upload URL response did not contain upload URLs.")
|
||||
})?
|
||||
.clone();
|
||||
for url in &urls {
|
||||
if parts_uploaded >= total_parts {
|
||||
@@ -197,14 +223,23 @@ impl ApsClient {
|
||||
let percent = if file_size == 0 {
|
||||
100
|
||||
} else {
|
||||
min(100, ((bytes_uploaded as f64 / file_size as f64) * 100.0) as i32)
|
||||
min(
|
||||
100,
|
||||
((bytes_uploaded as f64 / file_size as f64) * 100.0) as i32,
|
||||
)
|
||||
};
|
||||
cb(&file_name, Some(percent), Some(bytes_uploaded), Some(file_size));
|
||||
cb(
|
||||
&file_name,
|
||||
Some(percent),
|
||||
Some(bytes_uploaded),
|
||||
Some(file_size),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key = upload_key.ok_or_else(|| RpcError::internal("Upload did not return an upload key."))?;
|
||||
let key =
|
||||
upload_key.ok_or_else(|| RpcError::internal("Upload did not return an upload key."))?;
|
||||
self.complete_signed_upload(bucket, object, &key)
|
||||
}
|
||||
|
||||
@@ -216,7 +251,9 @@ impl ApsClient {
|
||||
first_part: u32,
|
||||
parts: u32,
|
||||
) -> Result<Value, RpcError> {
|
||||
let token = self.auth.ensure_access_token(crate::progress::noop_auth())?;
|
||||
let token = self
|
||||
.auth
|
||||
.ensure_access_token(crate::progress::noop_auth())?;
|
||||
let mut url = format!(
|
||||
"{}/oss/v2/buckets/{}/objects/{}/signeds3upload?minutesExpiration=10&firstPart={}&parts={}",
|
||||
self.base_url,
|
||||
@@ -236,8 +273,15 @@ impl ApsClient {
|
||||
decode_json(resp, &url)
|
||||
}
|
||||
|
||||
fn complete_signed_upload(&self, bucket: &str, object: &str, upload_key: &str) -> Result<(), RpcError> {
|
||||
let token = self.auth.ensure_access_token(crate::progress::noop_auth())?;
|
||||
fn complete_signed_upload(
|
||||
&self,
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
upload_key: &str,
|
||||
) -> Result<(), RpcError> {
|
||||
let token = self
|
||||
.auth
|
||||
.ensure_access_token(crate::progress::noop_auth())?;
|
||||
let url = format!(
|
||||
"{}/oss/v2/buckets/{}/objects/{}/signeds3upload",
|
||||
self.base_url,
|
||||
@@ -254,7 +298,11 @@ impl ApsClient {
|
||||
Ok(_) => Ok(()),
|
||||
Err(ureq::Error::Status(code, resp)) => {
|
||||
let body = resp.into_string().unwrap_or_default();
|
||||
let msg = if body.trim().is_empty() { format!("HTTP {code}") } else { body.trim().to_string() };
|
||||
let msg = if body.trim().is_empty() {
|
||||
format!("HTTP {code}")
|
||||
} else {
|
||||
body.trim().to_string()
|
||||
};
|
||||
Err(RpcError::internal(msg))
|
||||
}
|
||||
Err(e) => Err(RpcError::internal(e.to_string())),
|
||||
@@ -271,7 +319,11 @@ impl ApsClient {
|
||||
Ok(_) => Ok(()),
|
||||
Err(ureq::Error::Status(code, resp)) => {
|
||||
let body = resp.into_string().unwrap_or_default();
|
||||
let msg = if body.trim().is_empty() { format!("HTTP {code}") } else { body.trim().to_string() };
|
||||
let msg = if body.trim().is_empty() {
|
||||
format!("HTTP {code}")
|
||||
} else {
|
||||
body.trim().to_string()
|
||||
};
|
||||
Err(RpcError::internal(msg))
|
||||
}
|
||||
Err(e) => Err(RpcError::internal(e.to_string())),
|
||||
@@ -299,7 +351,11 @@ impl ApsClient {
|
||||
storage_id: &str,
|
||||
) -> Result<UploadResult, RpcError> {
|
||||
let payload = self.post_json(
|
||||
&format!("{}/data/v1/projects/{}/versions", self.base_url, url_enc(project_id)),
|
||||
&format!(
|
||||
"{}/data/v1/projects/{}/versions",
|
||||
self.base_url,
|
||||
url_enc(project_id)
|
||||
),
|
||||
&json!({
|
||||
"jsonapi": {"version": "1.0"},
|
||||
"data": {
|
||||
@@ -318,14 +374,27 @@ impl ApsClient {
|
||||
let version = payload
|
||||
.get("data")
|
||||
.ok_or_else(|| RpcError::internal("Create version response missing 'data'."))?;
|
||||
let attrs = version.get("attributes").cloned().unwrap_or_else(|| json!({}));
|
||||
let attrs = version
|
||||
.get("attributes")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
Ok(UploadResult {
|
||||
item_id: item_id.to_string(),
|
||||
version_id: version.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||
version_id: version
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
display_name: file_name.to_string(),
|
||||
version_number: attrs.get("versionNumber").cloned(),
|
||||
last_modified_time_utc: attrs.get("lastModifiedTime").and_then(|v| v.as_str()).map(String::from),
|
||||
last_modified_user_name: attrs.get("lastModifiedUserName").and_then(|v| v.as_str()).map(String::from),
|
||||
last_modified_time_utc: attrs
|
||||
.get("lastModifiedTime")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
last_modified_user_name: attrs
|
||||
.get("lastModifiedUserName")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -337,7 +406,11 @@ impl ApsClient {
|
||||
storage_id: &str,
|
||||
) -> Result<UploadResult, RpcError> {
|
||||
let payload = self.post_json(
|
||||
&format!("{}/data/v1/projects/{}/items", self.base_url, url_enc(project_id)),
|
||||
&format!(
|
||||
"{}/data/v1/projects/{}/items",
|
||||
self.base_url,
|
||||
url_enc(project_id)
|
||||
),
|
||||
&json!({
|
||||
"jsonapi": {"version": "1.0"},
|
||||
"data": {
|
||||
@@ -386,8 +459,14 @@ impl ApsClient {
|
||||
if let Some(v) = attrs.get("versionNumber") {
|
||||
version_number = Some(v.clone());
|
||||
}
|
||||
last_modified_time = attrs.get("lastModifiedTime").and_then(|v| v.as_str()).map(String::from);
|
||||
last_modified_user = attrs.get("lastModifiedUserName").and_then(|v| v.as_str()).map(String::from);
|
||||
last_modified_time = attrs
|
||||
.get("lastModifiedTime")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
last_modified_user = attrs
|
||||
.get("lastModifiedUserName")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,14 +483,16 @@ impl ApsClient {
|
||||
|
||||
pub(crate) fn parse_storage_id(storage_id: &str) -> Result<(String, String), RpcError> {
|
||||
let marker = "urn:adsk.objects:os.object:";
|
||||
let path = storage_id
|
||||
.strip_prefix(marker)
|
||||
.ok_or_else(|| RpcError::internal(format!("Unsupported storage identifier '{storage_id}'.")))?;
|
||||
let slash = path
|
||||
.find('/')
|
||||
.ok_or_else(|| RpcError::internal(format!("Malformed storage identifier '{storage_id}'.")))?;
|
||||
let path = storage_id.strip_prefix(marker).ok_or_else(|| {
|
||||
RpcError::internal(format!("Unsupported storage identifier '{storage_id}'."))
|
||||
})?;
|
||||
let slash = path.find('/').ok_or_else(|| {
|
||||
RpcError::internal(format!("Malformed storage identifier '{storage_id}'."))
|
||||
})?;
|
||||
if slash == 0 || slash == path.len() - 1 {
|
||||
return Err(RpcError::internal(format!("Malformed storage identifier '{storage_id}'.")));
|
||||
return Err(RpcError::internal(format!(
|
||||
"Malformed storage identifier '{storage_id}'."
|
||||
)));
|
||||
}
|
||||
Ok((path[..slash].to_string(), path[slash + 1..].to_string()))
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ use crate::progress::AuthProgress;
|
||||
use crate::rpc::RpcError;
|
||||
|
||||
pub const KEYRING_SERVICE: &str = "bonsaiviewer-autodesk";
|
||||
pub const AUTHORIZE_ENDPOINT: &str = "https://developer.api.autodesk.com/authentication/v2/authorize";
|
||||
pub const AUTHORIZE_ENDPOINT: &str =
|
||||
"https://developer.api.autodesk.com/authentication/v2/authorize";
|
||||
pub const TOKEN_ENDPOINT: &str = "https://developer.api.autodesk.com/authentication/v2/token";
|
||||
|
||||
const REFRESH_TTL_DEFAULT_SECONDS: i64 = 15 * 24 * 60 * 60;
|
||||
@@ -44,7 +45,10 @@ pub struct KeyringTokenStore {
|
||||
|
||||
impl KeyringTokenStore {
|
||||
pub fn new(username: impl Into<String>) -> Self {
|
||||
Self { service: KEYRING_SERVICE.to_string(), username: username.into() }
|
||||
Self {
|
||||
service: KEYRING_SERVICE.to_string(),
|
||||
username: username.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn entry(&self) -> Result<keyring::Entry, RpcError> {
|
||||
@@ -128,8 +132,9 @@ pub fn wait_for_oauth_callback(
|
||||
path: &str,
|
||||
expected_state: &str,
|
||||
) -> Result<String, RpcError> {
|
||||
let listener = TcpListener::bind((host, port))
|
||||
.map_err(|e| RpcError::internal(format!("Cannot bind OAuth callback to {host}:{port}: {e}")))?;
|
||||
let listener = TcpListener::bind((host, port)).map_err(|e| {
|
||||
RpcError::internal(format!("Cannot bind OAuth callback to {host}:{port}: {e}"))
|
||||
})?;
|
||||
let (mut stream, _) = listener
|
||||
.accept()
|
||||
.map_err(|e| RpcError::internal(format!("OAuth callback accept failed: {e}")))?;
|
||||
@@ -137,7 +142,9 @@ pub fn wait_for_oauth_callback(
|
||||
|
||||
let request_line = {
|
||||
let mut reader = BufReader::new(
|
||||
stream.try_clone().map_err(|e| RpcError::internal(e.to_string()))?,
|
||||
stream
|
||||
.try_clone()
|
||||
.map_err(|e| RpcError::internal(e.to_string()))?,
|
||||
);
|
||||
let mut line = String::new();
|
||||
reader
|
||||
@@ -184,7 +191,9 @@ pub fn wait_for_oauth_callback(
|
||||
}
|
||||
|
||||
if let Some(err) = error.filter(|s| !s.is_empty()) {
|
||||
return Err(RpcError::internal(format!("Autodesk returned OAuth error '{err}'.")));
|
||||
return Err(RpcError::internal(format!(
|
||||
"Autodesk returned OAuth error '{err}'."
|
||||
)));
|
||||
}
|
||||
if state.as_deref() != Some(expected_state) {
|
||||
return Err(RpcError::internal("OAuth state mismatch."));
|
||||
@@ -195,10 +204,11 @@ pub fn wait_for_oauth_callback(
|
||||
|
||||
/// Pluggable callback strategy — production binds a localhost socket, tests
|
||||
/// inject a stub that returns a canned code.
|
||||
pub type CallbackWaiter = Arc<dyn Fn(&str, u16, &str, &str) -> Result<String, RpcError> + Send + Sync>;
|
||||
pub type CallbackWaiter =
|
||||
Arc<dyn Fn(&str, u16, &str, &str) -> Result<String, RpcError> + Send + Sync>;
|
||||
|
||||
pub fn default_callback_waiter() -> CallbackWaiter {
|
||||
Arc::new(|host, port, path, state| wait_for_oauth_callback(host, port, path, state))
|
||||
Arc::new(wait_for_oauth_callback)
|
||||
}
|
||||
|
||||
/// Pluggable "open this URL in a browser" — production opens it for real,
|
||||
@@ -235,7 +245,12 @@ pub struct AuthBuilder {
|
||||
}
|
||||
|
||||
impl AuthBuilder {
|
||||
pub fn new(client_id: String, callback_url: String, scope: String, token_store: Box<dyn TokenStore>) -> Self {
|
||||
pub fn new(
|
||||
client_id: String,
|
||||
callback_url: String,
|
||||
scope: String,
|
||||
token_store: Box<dyn TokenStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client_id,
|
||||
callback_url,
|
||||
@@ -317,7 +332,9 @@ impl AuthSessionService {
|
||||
if callback.scheme() != "http"
|
||||
|| !matches!(callback.host_str(), Some("127.0.0.1") | Some("localhost"))
|
||||
{
|
||||
return Err(RpcError::internal("Callback URL must be http://localhost or http://127.0.0.1."));
|
||||
return Err(RpcError::internal(
|
||||
"Callback URL must be http://localhost or http://127.0.0.1.",
|
||||
));
|
||||
}
|
||||
|
||||
let authorize_url = {
|
||||
@@ -356,7 +373,11 @@ impl AuthSessionService {
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn refresh(&self, token: &StoredToken, progress: AuthProgress) -> Result<StoredToken, RpcError> {
|
||||
fn refresh(
|
||||
&self,
|
||||
token: &StoredToken,
|
||||
progress: AuthProgress,
|
||||
) -> Result<StoredToken, RpcError> {
|
||||
progress("auth", "Refreshing Autodesk session", None);
|
||||
let payload = self
|
||||
.post_form(&[
|
||||
@@ -408,7 +429,8 @@ impl AuthSessionService {
|
||||
access_token: access_token.to_string(),
|
||||
refresh_token: refresh_token.to_string(),
|
||||
access_token_expires_at: now + ChronoDuration::seconds(expires_in - TOKEN_SKEW_SECONDS),
|
||||
refresh_token_expires_at: now + ChronoDuration::seconds(refresh_ttl - TOKEN_SKEW_SECONDS),
|
||||
refresh_token_expires_at: now
|
||||
+ ChronoDuration::seconds(refresh_ttl - TOKEN_SKEW_SECONDS),
|
||||
scope: self.scope.clone(),
|
||||
})
|
||||
}
|
||||
@@ -421,10 +443,14 @@ pub struct InMemoryTokenStore {
|
||||
|
||||
impl InMemoryTokenStore {
|
||||
pub fn empty() -> Self {
|
||||
Self { inner: std::sync::Mutex::new(None) }
|
||||
Self {
|
||||
inner: std::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
pub fn preloaded(token: StoredToken) -> Self {
|
||||
Self { inner: std::sync::Mutex::new(Some(token)) }
|
||||
Self {
|
||||
inner: std::sync::Mutex::new(Some(token)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +509,11 @@ mod tests {
|
||||
assert!(store.load().unwrap().is_none());
|
||||
}
|
||||
|
||||
fn make_service(store: Box<dyn TokenStore>, token_endpoint: String, code: &'static str) -> AuthSessionService {
|
||||
fn make_service(
|
||||
store: Box<dyn TokenStore>,
|
||||
token_endpoint: String,
|
||||
code: &'static str,
|
||||
) -> AuthSessionService {
|
||||
AuthBuilder::new(
|
||||
"client-xyz".into(),
|
||||
"http://127.0.0.1:8080/".into(),
|
||||
@@ -539,15 +569,22 @@ mod tests {
|
||||
inner: InMemoryTokenStore,
|
||||
}
|
||||
impl TokenStore for PeekStore {
|
||||
fn load(&self) -> Result<Option<StoredToken>, RpcError> { self.inner.load() }
|
||||
fn load(&self) -> Result<Option<StoredToken>, RpcError> {
|
||||
self.inner.load()
|
||||
}
|
||||
fn save(&self, t: &StoredToken) -> Result<(), RpcError> {
|
||||
*self.shadow.lock().unwrap() = Some(t.clone());
|
||||
self.inner.save(t)
|
||||
}
|
||||
fn delete(&self) -> Result<(), RpcError> { self.inner.delete() }
|
||||
fn delete(&self) -> Result<(), RpcError> {
|
||||
self.inner.delete()
|
||||
}
|
||||
}
|
||||
let _ = store;
|
||||
let peek = Box::new(PeekStore { shadow: saved_check.clone(), inner: InMemoryTokenStore::empty() });
|
||||
let peek = Box::new(PeekStore {
|
||||
shadow: saved_check.clone(),
|
||||
inner: InMemoryTokenStore::empty(),
|
||||
});
|
||||
|
||||
let svc = make_service(peek, endpoint, "AUTH_CODE");
|
||||
let token = svc.login_interactive(crate::progress::noop_auth()).unwrap();
|
||||
@@ -570,7 +607,9 @@ mod tests {
|
||||
};
|
||||
let store = Box::new(InMemoryTokenStore::preloaded(token));
|
||||
let svc = make_service(store, "http://127.0.0.1:1/never".into(), "x");
|
||||
let access = svc.ensure_access_token(crate::progress::noop_auth()).unwrap();
|
||||
let access = svc
|
||||
.ensure_access_token(crate::progress::noop_auth())
|
||||
.unwrap();
|
||||
assert_eq!(access, "CACHED");
|
||||
}
|
||||
|
||||
@@ -591,7 +630,9 @@ mod tests {
|
||||
};
|
||||
let store = Box::new(InMemoryTokenStore::preloaded(token));
|
||||
let svc = make_service(store, endpoint, "unused");
|
||||
let access = svc.ensure_access_token(crate::progress::noop_auth()).unwrap();
|
||||
let access = svc
|
||||
.ensure_access_token(crate::progress::noop_auth())
|
||||
.unwrap();
|
||||
assert_eq!(access, "FRESH");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,17 @@ fn hash_parts(parts: &[&str]) -> String {
|
||||
/// Stable directory for an .ifcfed; re-downloads overwrite in place so the
|
||||
/// viewer's open path remains valid across sync operations.
|
||||
pub fn ifcfed_dir(project_id: &str, item_id: &str) -> PathBuf {
|
||||
cache_root().join("ifcfeds").join(hash_parts(&[project_id, item_id]))
|
||||
cache_root()
|
||||
.join("ifcfeds")
|
||||
.join(hash_parts(&[project_id, item_id]))
|
||||
}
|
||||
|
||||
/// Per-version directory for a model. A new resolved version → a new
|
||||
/// directory, so sidecars regenerate when the model file changes.
|
||||
pub fn model_dir(project_id: &str, item_id: &str, version_id: &str) -> PathBuf {
|
||||
cache_root().join("models").join(hash_parts(&[project_id, item_id, version_id]))
|
||||
cache_root()
|
||||
.join("models")
|
||||
.join(hash_parts(&[project_id, item_id, version_id]))
|
||||
}
|
||||
|
||||
/// Clear the directory so the file we write is the only child.
|
||||
|
||||
@@ -83,9 +83,17 @@ pub struct AutodeskConnector {
|
||||
inner: RefCell<Option<ClientPair>>,
|
||||
}
|
||||
|
||||
impl Default for AutodeskConnector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AutodeskConnector {
|
||||
pub fn new() -> Self {
|
||||
let me = Self { inner: RefCell::new(None) };
|
||||
let me = Self {
|
||||
inner: RefCell::new(None),
|
||||
};
|
||||
me.reload_credentials();
|
||||
me
|
||||
}
|
||||
@@ -144,7 +152,7 @@ impl AutodeskConnector {
|
||||
// SAFETY: SettingsDialog::run blocks the calling thread, and the
|
||||
// callback fires only on this thread before run returns. The pointer
|
||||
// outlives the call because `self` is borrowed by the RPC dispatch.
|
||||
let on_reload: Arc<dyn Fn() -> Result<(), RpcError>> = Arc::new(move || {
|
||||
let on_reload: Rc<dyn Fn() -> Result<(), RpcError>> = Rc::new(move || {
|
||||
let cell: &RefCell<Option<ClientPair>> = unsafe { &*inner_ptr };
|
||||
let client_id = cfg::load_client_id();
|
||||
if client_id.is_empty() {
|
||||
@@ -181,7 +189,14 @@ impl AutodeskConnector {
|
||||
let display_name = entry.display_name.clone();
|
||||
let aps = pair.aps.clone();
|
||||
let path = run_with_progress("Downloading project", move |report| {
|
||||
download_ifcfed(&aps, &hub_id, &project_id, &item_id, &display_name, Some(download_callback(report, 0, 0)))
|
||||
download_ifcfed(
|
||||
&aps,
|
||||
&hub_id,
|
||||
&project_id,
|
||||
&item_id,
|
||||
&display_name,
|
||||
Some(download_callback(report, 0, 0)),
|
||||
)
|
||||
})?;
|
||||
Ok(json!({"path": path.to_string_lossy()}))
|
||||
}
|
||||
@@ -190,13 +205,19 @@ impl AutodeskConnector {
|
||||
|
||||
fn pull_ifcfed(&self, params: Value) -> Result<Value, RpcError> {
|
||||
let pair = self.require()?;
|
||||
let p: PullIfcfedParams = serde_json::from_value(params).map_err(|e| {
|
||||
RpcError::invalid_params(format!("pull_ifcfed: {e}"))
|
||||
})?;
|
||||
let p: PullIfcfedParams = serde_json::from_value(params)
|
||||
.map_err(|e| RpcError::invalid_params(format!("pull_ifcfed: {e}")))?;
|
||||
let display = p.display_name.unwrap_or_else(|| p.item_id.clone());
|
||||
let aps = pair.aps.clone();
|
||||
let path = run_with_progress("Downloading project", move |report| {
|
||||
download_ifcfed(&aps, &p.hub_id, &p.project_id, &p.item_id, &display, Some(download_callback(report, 0, 0)))
|
||||
download_ifcfed(
|
||||
&aps,
|
||||
&p.hub_id,
|
||||
&p.project_id,
|
||||
&p.item_id,
|
||||
&display,
|
||||
Some(download_callback(report, 0, 0)),
|
||||
)
|
||||
})?;
|
||||
Ok(json!({"path": path.to_string_lossy()}))
|
||||
}
|
||||
@@ -205,9 +226,8 @@ impl AutodeskConnector {
|
||||
|
||||
fn pull_models(&self, params: Value) -> Result<Value, RpcError> {
|
||||
let pair = self.require()?;
|
||||
let entries: Vec<PullModelEntry> = serde_json::from_value(params).map_err(|e| {
|
||||
RpcError::invalid_params(format!("pull_models: {e}"))
|
||||
})?;
|
||||
let entries: Vec<PullModelEntry> = serde_json::from_value(params)
|
||||
.map_err(|e| RpcError::invalid_params(format!("pull_models: {e}")))?;
|
||||
let total = entries.len();
|
||||
let aps = pair.aps.clone();
|
||||
let results = run_with_progress("Downloading models", move |report| {
|
||||
@@ -241,7 +261,14 @@ impl AutodeskConnector {
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for (index, entry) in chosen.entries.into_iter().enumerate() {
|
||||
let cb = download_callback(report.clone(), index + 1, total);
|
||||
match download_picked_model(&aps, &hub.id, &project.id, &entry.id, &entry.display_name, Some(cb)) {
|
||||
match download_picked_model(
|
||||
&aps,
|
||||
&hub.id,
|
||||
&project.id,
|
||||
&entry.id,
|
||||
&entry.display_name,
|
||||
Some(cb),
|
||||
) {
|
||||
Ok(Some(v)) => out.push(v),
|
||||
Ok(None) => {}
|
||||
Err(e) => eprintln!("pull_models_interactive[{index}] skipped: {}", e.message),
|
||||
@@ -256,16 +283,18 @@ impl AutodeskConnector {
|
||||
|
||||
fn push_ifcfed_interactive(&self, params: Value) -> Result<Value, RpcError> {
|
||||
let pair = self.require()?;
|
||||
let p: PushInteractiveParams = serde_json::from_value(params).map_err(|e| {
|
||||
RpcError::invalid_params(format!("push_ifcfed_interactive: {e}"))
|
||||
})?;
|
||||
let p: PushInteractiveParams = serde_json::from_value(params)
|
||||
.map_err(|e| RpcError::invalid_params(format!("push_ifcfed_interactive: {e}")))?;
|
||||
check_local_exists(&p.path)?;
|
||||
let default_name = file_name_of(&p.path);
|
||||
if !default_name.to_lowercase().ends_with(".ifcfed") {
|
||||
return Err(RpcError::invalid_params("push_ifcfed_interactive expects an .ifcfed file."));
|
||||
return Err(RpcError::invalid_params(
|
||||
"push_ifcfed_interactive expects an .ifcfed file.",
|
||||
));
|
||||
}
|
||||
|
||||
let chosen = BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?;
|
||||
let chosen =
|
||||
BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?;
|
||||
let folder = chosen
|
||||
.entries
|
||||
.first()
|
||||
@@ -274,7 +303,11 @@ impl AutodeskConnector {
|
||||
|
||||
let raw = prompt_for_filename("Save Project", "Save .ifcfed as:", &default_name)
|
||||
.ok_or_else(|| RpcError::internal("User cancelled save to cloud."))?;
|
||||
let file_name = if raw.to_lowercase().ends_with(".ifcfed") { raw } else { format!("{raw}.ifcfed") };
|
||||
let file_name = if raw.to_lowercase().ends_with(".ifcfed") {
|
||||
raw
|
||||
} else {
|
||||
format!("{raw}.ifcfed")
|
||||
};
|
||||
|
||||
let project_id = chosen.project.id.clone();
|
||||
let folder_id = folder.id.clone();
|
||||
@@ -282,17 +315,28 @@ impl AutodeskConnector {
|
||||
let local_path = p.path.clone();
|
||||
let upload_name = file_name.clone();
|
||||
let uploaded = run_with_progress("Uploading project", move |report| {
|
||||
aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report)))
|
||||
aps.upload_file_to_folder(
|
||||
&project_id,
|
||||
&folder_id,
|
||||
&local_path,
|
||||
Some(&upload_name),
|
||||
Some(upload_callback(report)),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cached_path = cache_ifcfed_locally(&chosen.project.id, &uploaded.item_id, &file_name, &p.path)?;
|
||||
cache::write_manifest(&cached_path, &Manifest {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
hub_id: chosen.hub.id,
|
||||
project_id: chosen.project.id,
|
||||
item_id: uploaded.item_id,
|
||||
display_name: file_name,
|
||||
}).map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?;
|
||||
let cached_path =
|
||||
cache_ifcfed_locally(&chosen.project.id, &uploaded.item_id, &file_name, &p.path)?;
|
||||
cache::write_manifest(
|
||||
&cached_path,
|
||||
&Manifest {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
hub_id: chosen.hub.id,
|
||||
project_id: chosen.project.id,
|
||||
item_id: uploaded.item_id,
|
||||
display_name: file_name,
|
||||
},
|
||||
)
|
||||
.map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?;
|
||||
Ok(json!({"path": cached_path.to_string_lossy()}))
|
||||
}
|
||||
|
||||
@@ -300,23 +344,30 @@ impl AutodeskConnector {
|
||||
|
||||
fn push_ifcfed(&self, params: Value) -> Result<Value, RpcError> {
|
||||
let pair = self.require()?;
|
||||
let p: PushIfcfedParams = serde_json::from_value(params).map_err(|e| {
|
||||
RpcError::invalid_params(format!("push_ifcfed: {e}"))
|
||||
})?;
|
||||
let p: PushIfcfedParams = serde_json::from_value(params)
|
||||
.map_err(|e| RpcError::invalid_params(format!("push_ifcfed: {e}")))?;
|
||||
check_local_exists(&p.path)?;
|
||||
if !file_name_of(&p.path).to_lowercase().ends_with(".ifcfed") {
|
||||
return Err(RpcError::invalid_params("push_ifcfed expects an .ifcfed file."));
|
||||
return Err(RpcError::invalid_params(
|
||||
"push_ifcfed expects an .ifcfed file.",
|
||||
));
|
||||
}
|
||||
if p.manifest.connector != CONNECTOR_ID {
|
||||
return Err(RpcError::invalid_params(format!("Manifest connector is not '{CONNECTOR_ID}'.")));
|
||||
return Err(RpcError::invalid_params(format!(
|
||||
"Manifest connector is not '{CONNECTOR_ID}'."
|
||||
)));
|
||||
}
|
||||
|
||||
let item = pair.aps.get_item(&p.manifest.project_id, &p.manifest.item_id)?;
|
||||
let item = pair
|
||||
.aps
|
||||
.get_item(&p.manifest.project_id, &p.manifest.item_id)?;
|
||||
ensure_visible(&item)?;
|
||||
let folder_id = item
|
||||
.parent_folder_id
|
||||
.clone()
|
||||
.ok_or_else(|| RpcError::internal(format!("Cannot resolve parent folder for item '{}'.", p.manifest.item_id)))?;
|
||||
let folder_id = item.parent_folder_id.clone().ok_or_else(|| {
|
||||
RpcError::internal(format!(
|
||||
"Cannot resolve parent folder for item '{}'.",
|
||||
p.manifest.item_id
|
||||
))
|
||||
})?;
|
||||
let file_name = if p.manifest.display_name.is_empty() {
|
||||
item.display_name.clone()
|
||||
} else {
|
||||
@@ -328,17 +379,32 @@ impl AutodeskConnector {
|
||||
let project_id = p.manifest.project_id.clone();
|
||||
let upload_name = file_name.clone();
|
||||
let uploaded = run_with_progress("Uploading project", move |report| {
|
||||
aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report)))
|
||||
aps.upload_file_to_folder(
|
||||
&project_id,
|
||||
&folder_id,
|
||||
&local_path,
|
||||
Some(&upload_name),
|
||||
Some(upload_callback(report)),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cached_path = cache_ifcfed_locally(&p.manifest.project_id, &uploaded.item_id, &file_name, &p.path)?;
|
||||
cache::write_manifest(&cached_path, &Manifest {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
hub_id: p.manifest.hub_id,
|
||||
project_id: p.manifest.project_id,
|
||||
item_id: uploaded.item_id,
|
||||
display_name: file_name,
|
||||
}).map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?;
|
||||
let cached_path = cache_ifcfed_locally(
|
||||
&p.manifest.project_id,
|
||||
&uploaded.item_id,
|
||||
&file_name,
|
||||
&p.path,
|
||||
)?;
|
||||
cache::write_manifest(
|
||||
&cached_path,
|
||||
&Manifest {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
hub_id: p.manifest.hub_id,
|
||||
project_id: p.manifest.project_id,
|
||||
item_id: uploaded.item_id,
|
||||
display_name: file_name,
|
||||
},
|
||||
)
|
||||
.map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?;
|
||||
Ok(json!({"path": cached_path.to_string_lossy()}))
|
||||
}
|
||||
|
||||
@@ -346,12 +412,12 @@ impl AutodeskConnector {
|
||||
|
||||
fn push_model_interactive(&self, params: Value) -> Result<Value, RpcError> {
|
||||
let pair = self.require()?;
|
||||
let p: PushInteractiveParams = serde_json::from_value(params).map_err(|e| {
|
||||
RpcError::invalid_params(format!("push_model_interactive: {e}"))
|
||||
})?;
|
||||
let p: PushInteractiveParams = serde_json::from_value(params)
|
||||
.map_err(|e| RpcError::invalid_params(format!("push_model_interactive: {e}")))?;
|
||||
check_local_exists(&p.path)?;
|
||||
|
||||
let chosen = BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?;
|
||||
let chosen =
|
||||
BrowseDialog::new(pair.auth.clone(), pair.aps.clone(), Mode::Destination).run()?;
|
||||
let folder = chosen
|
||||
.entries
|
||||
.first()
|
||||
@@ -368,10 +434,22 @@ impl AutodeskConnector {
|
||||
let local_path = p.path.clone();
|
||||
let upload_name = file_name.clone();
|
||||
let uploaded = run_with_progress("Uploading model", move |report| {
|
||||
aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report)))
|
||||
aps.upload_file_to_folder(
|
||||
&project_id,
|
||||
&folder_id,
|
||||
&local_path,
|
||||
Some(&upload_name),
|
||||
Some(upload_callback(report)),
|
||||
)
|
||||
})?;
|
||||
|
||||
let cached_path = cache_model_locally(&chosen.project.id, &uploaded.item_id, &uploaded.version_id, &file_name, &p.path)?;
|
||||
let cached_path = cache_model_locally(
|
||||
&chosen.project.id,
|
||||
&uploaded.item_id,
|
||||
&uploaded.version_id,
|
||||
&file_name,
|
||||
&p.path,
|
||||
)?;
|
||||
Ok(json!({
|
||||
"display_name": file_name,
|
||||
"path": cached_path.to_string_lossy(),
|
||||
@@ -389,31 +467,50 @@ impl AutodeskConnector {
|
||||
|
||||
fn push_model(&self, params: Value) -> Result<Value, RpcError> {
|
||||
let pair = self.require()?;
|
||||
let p: PushModelParams = serde_json::from_value(params).map_err(|e| {
|
||||
RpcError::invalid_params(format!("push_model: {e}"))
|
||||
})?;
|
||||
let p: PushModelParams = serde_json::from_value(params)
|
||||
.map_err(|e| RpcError::invalid_params(format!("push_model: {e}")))?;
|
||||
check_local_exists(&p.path)?;
|
||||
if p.source.connector != CONNECTOR_ID {
|
||||
return Err(RpcError::invalid_params(format!("Source connector is not '{CONNECTOR_ID}'.")));
|
||||
return Err(RpcError::invalid_params(format!(
|
||||
"Source connector is not '{CONNECTOR_ID}'."
|
||||
)));
|
||||
}
|
||||
|
||||
let item = pair.aps.get_item(&p.source.project_id, &p.source.item_id)?;
|
||||
ensure_visible(&item)?;
|
||||
let folder_id = item
|
||||
.parent_folder_id
|
||||
.clone()
|
||||
.ok_or_else(|| RpcError::internal(format!("Cannot resolve parent folder for item '{}'.", p.source.item_id)))?;
|
||||
let file_name = if item.display_name.is_empty() { file_name_of(&p.path) } else { item.display_name.clone() };
|
||||
let folder_id = item.parent_folder_id.clone().ok_or_else(|| {
|
||||
RpcError::internal(format!(
|
||||
"Cannot resolve parent folder for item '{}'.",
|
||||
p.source.item_id
|
||||
))
|
||||
})?;
|
||||
let file_name = if item.display_name.is_empty() {
|
||||
file_name_of(&p.path)
|
||||
} else {
|
||||
item.display_name.clone()
|
||||
};
|
||||
|
||||
let aps = pair.aps.clone();
|
||||
let local_path = p.path.clone();
|
||||
let project_id = p.source.project_id.clone();
|
||||
let upload_name = file_name.clone();
|
||||
let uploaded = run_with_progress("Uploading model", move |report| {
|
||||
aps.upload_file_to_folder(&project_id, &folder_id, &local_path, Some(&upload_name), Some(upload_callback(report)))
|
||||
aps.upload_file_to_folder(
|
||||
&project_id,
|
||||
&folder_id,
|
||||
&local_path,
|
||||
Some(&upload_name),
|
||||
Some(upload_callback(report)),
|
||||
)
|
||||
})?;
|
||||
|
||||
let _cached_path = cache_model_locally(&p.source.project_id, &uploaded.item_id, &uploaded.version_id, &file_name, &p.path)?;
|
||||
let _cached_path = cache_model_locally(
|
||||
&p.source.project_id,
|
||||
&uploaded.item_id,
|
||||
&uploaded.version_id,
|
||||
&file_name,
|
||||
&p.path,
|
||||
)?;
|
||||
Ok(json!({
|
||||
"source": Source {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
@@ -438,10 +535,11 @@ fn download_ifcfed(
|
||||
) -> Result<PathBuf, RpcError> {
|
||||
let item = aps.get_item(project_id, item_id)?;
|
||||
ensure_visible(&item)?;
|
||||
let storage_id = item
|
||||
.storage_id
|
||||
.clone()
|
||||
.ok_or_else(|| RpcError::internal(format!("Autodesk item '{item_id}' has no downloadable storage.")))?;
|
||||
let storage_id = item.storage_id.clone().ok_or_else(|| {
|
||||
RpcError::internal(format!(
|
||||
"Autodesk item '{item_id}' has no downloadable storage."
|
||||
))
|
||||
})?;
|
||||
let file_name = if !item.display_name.is_empty() {
|
||||
item.display_name.clone()
|
||||
} else if !display_name_hint.is_empty() {
|
||||
@@ -450,19 +548,25 @@ fn download_ifcfed(
|
||||
item_id.to_string()
|
||||
};
|
||||
if !file_name.to_lowercase().ends_with(".ifcfed") {
|
||||
return Err(RpcError::internal(format!("Item '{file_name}' is not an .ifcfed file.")));
|
||||
return Err(RpcError::internal(format!(
|
||||
"Item '{file_name}' is not an .ifcfed file."
|
||||
)));
|
||||
}
|
||||
let directory = cache::prepare_sole_child_dir(&cache::ifcfed_dir(project_id, item_id))
|
||||
.map_err(|e| RpcError::internal(format!("Cache dir prep failed: {e}")))?;
|
||||
let ifcfed_path = directory.join(&file_name);
|
||||
aps.download_storage_to_file(&storage_id, &ifcfed_path, progress)?;
|
||||
cache::write_manifest(&ifcfed_path, &Manifest {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
hub_id: hub_id.into(),
|
||||
project_id: project_id.into(),
|
||||
item_id: item_id.into(),
|
||||
display_name: file_name,
|
||||
}).map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?;
|
||||
cache::write_manifest(
|
||||
&ifcfed_path,
|
||||
&Manifest {
|
||||
connector: CONNECTOR_ID.into(),
|
||||
hub_id: hub_id.into(),
|
||||
project_id: project_id.into(),
|
||||
item_id: item_id.into(),
|
||||
display_name: file_name,
|
||||
},
|
||||
)
|
||||
.map_err(|e| RpcError::internal(format!("Manifest write failed: {e}")))?;
|
||||
Ok(ifcfed_path)
|
||||
}
|
||||
|
||||
@@ -472,7 +576,9 @@ fn resolve_scripted_model(
|
||||
progress: Option<crate::progress::ApsProgress>,
|
||||
) -> Result<Option<Value>, RpcError> {
|
||||
if entry.source.connector != CONNECTOR_ID {
|
||||
return Err(RpcError::invalid_params(format!("Source connector is not '{CONNECTOR_ID}'.")));
|
||||
return Err(RpcError::invalid_params(format!(
|
||||
"Source connector is not '{CONNECTOR_ID}'."
|
||||
)));
|
||||
}
|
||||
let display_hint = entry
|
||||
.display_name
|
||||
@@ -481,14 +587,23 @@ fn resolve_scripted_model(
|
||||
|
||||
let item = aps.get_item(&entry.source.project_id, &entry.source.item_id)?;
|
||||
if item.hidden {
|
||||
eprintln!("Autodesk item '{}' is hidden/deleted; returning null.", entry.source.item_id);
|
||||
eprintln!(
|
||||
"Autodesk item '{}' is hidden/deleted; returning null.",
|
||||
entry.source.item_id
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
let storage_id = item
|
||||
.storage_id
|
||||
.clone()
|
||||
.ok_or_else(|| RpcError::internal(format!("Autodesk item '{}' has no downloadable storage.", entry.source.item_id)))?;
|
||||
let file_name = if item.display_name.is_empty() { display_hint } else { item.display_name.clone() };
|
||||
let storage_id = item.storage_id.clone().ok_or_else(|| {
|
||||
RpcError::internal(format!(
|
||||
"Autodesk item '{}' has no downloadable storage.",
|
||||
entry.source.item_id
|
||||
))
|
||||
})?;
|
||||
let file_name = if item.display_name.is_empty() {
|
||||
display_hint
|
||||
} else {
|
||||
item.display_name.clone()
|
||||
};
|
||||
let version_id = item.version_id.clone().unwrap_or_default();
|
||||
|
||||
let directory = cache::model_dir(&entry.source.project_id, &entry.source.item_id, &version_id);
|
||||
@@ -514,10 +629,11 @@ fn download_picked_model(
|
||||
) -> Result<Option<Value>, RpcError> {
|
||||
let item = aps.get_item(project_id, entry_id)?;
|
||||
ensure_visible(&item)?;
|
||||
let storage_id = item
|
||||
.storage_id
|
||||
.clone()
|
||||
.ok_or_else(|| RpcError::internal(format!("Autodesk item '{entry_id}' has no downloadable storage.")))?;
|
||||
let storage_id = item.storage_id.clone().ok_or_else(|| {
|
||||
RpcError::internal(format!(
|
||||
"Autodesk item '{entry_id}' has no downloadable storage."
|
||||
))
|
||||
})?;
|
||||
let file_name = if !item.display_name.is_empty() {
|
||||
item.display_name.clone()
|
||||
} else if !entry_display.is_empty() {
|
||||
@@ -548,20 +664,33 @@ fn download_picked_model(
|
||||
})))
|
||||
}
|
||||
|
||||
fn cache_ifcfed_locally(project_id: &str, item_id: &str, file_name: &str, src: &Path) -> Result<PathBuf, RpcError> {
|
||||
fn cache_ifcfed_locally(
|
||||
project_id: &str,
|
||||
item_id: &str,
|
||||
file_name: &str,
|
||||
src: &Path,
|
||||
) -> Result<PathBuf, RpcError> {
|
||||
let directory = cache::prepare_sole_child_dir(&cache::ifcfed_dir(project_id, item_id))
|
||||
.map_err(|e| RpcError::internal(format!("Cache dir prep failed: {e}")))?;
|
||||
let cached_path = directory.join(file_name);
|
||||
std::fs::copy(src, &cached_path).map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?;
|
||||
std::fs::copy(src, &cached_path)
|
||||
.map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?;
|
||||
Ok(cached_path)
|
||||
}
|
||||
|
||||
fn cache_model_locally(project_id: &str, item_id: &str, version_id: &str, file_name: &str, src: &Path) -> Result<PathBuf, RpcError> {
|
||||
fn cache_model_locally(
|
||||
project_id: &str,
|
||||
item_id: &str,
|
||||
version_id: &str,
|
||||
file_name: &str,
|
||||
src: &Path,
|
||||
) -> Result<PathBuf, RpcError> {
|
||||
let directory = cache::model_dir(project_id, item_id, version_id);
|
||||
cache::prepare_sole_child_dir(&directory)
|
||||
.map_err(|e| RpcError::internal(format!("Cache dir prep failed: {e}")))?;
|
||||
let cached_path = directory.join(file_name);
|
||||
std::fs::copy(src, &cached_path).map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?;
|
||||
std::fs::copy(src, &cached_path)
|
||||
.map_err(|e| RpcError::internal(format!("Cache copy failed: {e}")))?;
|
||||
Ok(cached_path)
|
||||
}
|
||||
|
||||
@@ -569,7 +698,10 @@ fn build_metadata_from_item(item: &ItemTip) -> Value {
|
||||
let mut metadata = serde_json::Map::new();
|
||||
if let Some(v) = &item.version_number {
|
||||
if !v.is_null() {
|
||||
metadata.insert("revision".into(), json!(format!("v{}", json_value_to_display(v))));
|
||||
metadata.insert(
|
||||
"revision".into(),
|
||||
json!(format!("v{}", json_value_to_display(v))),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(s) = &item.last_modified_time_utc {
|
||||
@@ -589,7 +721,10 @@ fn build_metadata_from_upload(uploaded: &UploadResult) -> Value {
|
||||
let mut metadata = serde_json::Map::new();
|
||||
if let Some(v) = &uploaded.version_number {
|
||||
if !v.is_null() {
|
||||
metadata.insert("revision".into(), json!(format!("v{}", json_value_to_display(v))));
|
||||
metadata.insert(
|
||||
"revision".into(),
|
||||
json!(format!("v{}", json_value_to_display(v))),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(s) = &uploaded.last_modified_time_utc {
|
||||
@@ -616,14 +751,20 @@ fn json_value_to_display(v: &Value) -> String {
|
||||
|
||||
fn ensure_visible(item: &ItemTip) -> Result<(), RpcError> {
|
||||
if item.hidden {
|
||||
return Err(RpcError::internal(format!("Autodesk item '{}' has been deleted.", item.id)));
|
||||
return Err(RpcError::internal(format!(
|
||||
"Autodesk item '{}' has been deleted.",
|
||||
item.id
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_local_exists(path: &Path) -> Result<(), RpcError> {
|
||||
if !path.exists() {
|
||||
return Err(RpcError::invalid_params(format!("Local file '{}' does not exist.", path.display())));
|
||||
return Err(RpcError::invalid_params(format!(
|
||||
"Local file '{}' does not exist.",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ pub fn progress_detail(percent: Option<i32>, done: Option<u64>, total: Option<u6
|
||||
parts.push(format!("{p}%"));
|
||||
}
|
||||
match (done, total) {
|
||||
(Some(d), Some(t)) if t > 0 => parts.push(format!("{} / {}", format_bytes(d), format_bytes(t))),
|
||||
(Some(d), Some(t)) if t > 0 => {
|
||||
parts.push(format!("{} / {}", format_bytes(d), format_bytes(t)))
|
||||
}
|
||||
(Some(d), _) => parts.push(format_bytes(d)),
|
||||
_ => {}
|
||||
}
|
||||
@@ -49,10 +51,18 @@ pub fn progress_detail(percent: Option<i32>, done: Option<u64>, total: Option<u6
|
||||
/// "(i/N)" when batching. Pass `total = 0` for single-file transfers.
|
||||
pub fn download_callback(report: Report, index: usize, total: usize) -> ApsProgress {
|
||||
Arc::new(move |name, percent, done, total_bytes| {
|
||||
let suffix = if total != 0 { format!(" ({index}/{total})") } else { String::new() };
|
||||
let suffix = if total != 0 {
|
||||
format!(" ({index}/{total})")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let detail = progress_detail(percent, done, total_bytes);
|
||||
let msg = format!("Downloading {name}{suffix}");
|
||||
let detail_ref = if detail.is_empty() { None } else { Some(detail.as_str()) };
|
||||
let detail_ref = if detail.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(detail.as_str())
|
||||
};
|
||||
report("download", &msg, percent, detail_ref);
|
||||
})
|
||||
}
|
||||
@@ -61,7 +71,11 @@ pub fn upload_callback(report: Report) -> ApsProgress {
|
||||
Arc::new(move |name, percent, done, total_bytes| {
|
||||
let detail = progress_detail(percent, done, total_bytes);
|
||||
let msg = format!("Uploading {name}");
|
||||
let detail_ref = if detail.is_empty() { None } else { Some(detail.as_str()) };
|
||||
let detail_ref = if detail.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(detail.as_str())
|
||||
};
|
||||
report("upload", &msg, percent, detail_ref);
|
||||
})
|
||||
}
|
||||
@@ -73,21 +87,31 @@ pub fn auth_to_report(report: Report) -> AuthProgress {
|
||||
})
|
||||
}
|
||||
|
||||
/// A single coalesced report: `(phase, message, percent, detail)`.
|
||||
type PendingReport = (String, String, Option<i32>, Option<String>);
|
||||
|
||||
/// Latest-wins hand-off from worker thread → UI thread. Intermediate updates
|
||||
/// are coalesced: only the freshest matters for a progress bar.
|
||||
pub struct ProgressBridge {
|
||||
pending: Mutex<Option<(String, String, Option<i32>, Option<String>)>>,
|
||||
pending: Mutex<Option<PendingReport>>,
|
||||
}
|
||||
|
||||
impl ProgressBridge {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self { pending: Mutex::new(None) })
|
||||
Arc::new(Self {
|
||||
pending: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn report_fn(self: Arc<Self>) -> Report {
|
||||
Arc::new(move |phase, message, percent, detail| {
|
||||
let mut slot = self.pending.lock().unwrap();
|
||||
*slot = Some((phase.to_string(), message.to_string(), percent, detail.map(str::to_string)));
|
||||
*slot = Some((
|
||||
phase.to_string(),
|
||||
message.to_string(),
|
||||
percent,
|
||||
detail.map(str::to_string),
|
||||
));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,7 +134,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn progress_detail_combines_parts() {
|
||||
assert_eq!(progress_detail(Some(50), Some(512), Some(1024)), "50%, 512 B / 1.0 KB");
|
||||
assert_eq!(
|
||||
progress_detail(Some(50), Some(512), Some(1024)),
|
||||
"50%, 512 B / 1.0 KB"
|
||||
);
|
||||
assert_eq!(progress_detail(None, Some(512), None), "512 B");
|
||||
assert_eq!(progress_detail(None, None, None), "");
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ pub struct RpcError {
|
||||
|
||||
impl RpcError {
|
||||
pub fn new(code: i32, message: impl Into<String>) -> Self {
|
||||
Self { code, message: message.into(), data: None }
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal(message: impl Into<String>) -> Self {
|
||||
@@ -48,7 +52,11 @@ pub struct JsonRpcHost<R: Read, W: Write> {
|
||||
|
||||
impl<R: Read, W: Write> JsonRpcHost<R, W> {
|
||||
pub fn new(handlers: HashMap<String, Handler>, stdin: R, stdout: W) -> Self {
|
||||
Self { handlers, stdin: BufReader::new(stdin), stdout }
|
||||
Self {
|
||||
handlers,
|
||||
stdin: BufReader::new(stdin),
|
||||
stdout,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> i32 {
|
||||
@@ -72,7 +80,12 @@ impl<R: Read, W: Write> JsonRpcHost<R, W> {
|
||||
let message: Value = match serde_json::from_str(line) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
self.respond_error(Value::Null, JSONRPC_PARSE_ERROR, &format!("Parse error: {e}"), None);
|
||||
self.respond_error(
|
||||
Value::Null,
|
||||
JSONRPC_PARSE_ERROR,
|
||||
&format!("Parse error: {e}"),
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -80,7 +93,12 @@ impl<R: Read, W: Write> JsonRpcHost<R, W> {
|
||||
let obj = match message.as_object() {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
self.respond_error(Value::Null, JSONRPC_INVALID_REQUEST, "Request must be a JSON object", None);
|
||||
self.respond_error(
|
||||
Value::Null,
|
||||
JSONRPC_INVALID_REQUEST,
|
||||
"Request must be a JSON object",
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -88,28 +106,48 @@ impl<R: Read, W: Write> JsonRpcHost<R, W> {
|
||||
let message_id = obj.get("id").cloned().unwrap_or(Value::Null);
|
||||
|
||||
if obj.get("jsonrpc").and_then(|v| v.as_str()) != Some("2.0") {
|
||||
self.respond_error(message_id, JSONRPC_INVALID_REQUEST, "Missing or wrong 'jsonrpc' version", None);
|
||||
self.respond_error(
|
||||
message_id,
|
||||
JSONRPC_INVALID_REQUEST,
|
||||
"Missing or wrong 'jsonrpc' version",
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let method = match obj.get("method").and_then(|v| v.as_str()) {
|
||||
Some(m) => m.to_string(),
|
||||
None => {
|
||||
self.respond_error(message_id, JSONRPC_INVALID_REQUEST, "Missing 'method' string", None);
|
||||
self.respond_error(
|
||||
message_id,
|
||||
JSONRPC_INVALID_REQUEST,
|
||||
"Missing 'method' string",
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let params = obj.get("params").cloned().unwrap_or(Value::Null);
|
||||
if !params.is_null() && !params.is_object() && !params.is_array() {
|
||||
self.respond_error(message_id, JSONRPC_INVALID_PARAMS, "'params' must be a JSON object or array", None);
|
||||
self.respond_error(
|
||||
message_id,
|
||||
JSONRPC_INVALID_PARAMS,
|
||||
"'params' must be a JSON object or array",
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let outcome = match self.handlers.get(&method) {
|
||||
Some(handler) => handler(params),
|
||||
None => {
|
||||
self.respond_error(message_id, JSONRPC_METHOD_NOT_FOUND, &format!("Unknown method '{method}'"), None);
|
||||
self.respond_error(
|
||||
message_id,
|
||||
JSONRPC_METHOD_NOT_FOUND,
|
||||
&format!("Unknown method '{method}'"),
|
||||
None,
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -168,15 +206,21 @@ mod tests {
|
||||
|
||||
fn echo_handlers() -> HashMap<String, Handler> {
|
||||
let mut m: HashMap<String, Handler> = HashMap::new();
|
||||
m.insert("echo".into(), Box::new(|p| Ok(p)));
|
||||
m.insert("echo".into(), Box::new(Ok));
|
||||
m.insert("boom".into(), Box::new(|_| Err(RpcError::internal("bang"))));
|
||||
m.insert("bad_params".into(), Box::new(|_| Err(RpcError::invalid_params("nope"))));
|
||||
m.insert(
|
||||
"bad_params".into(),
|
||||
Box::new(|_| Err(RpcError::invalid_params("nope"))),
|
||||
);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatches_result() {
|
||||
let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"x\":1}}\n");
|
||||
let out = run_once(
|
||||
echo_handlers(),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":{\"x\":1}}\n",
|
||||
);
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0]["result"], json!({"x": 1}));
|
||||
assert_eq!(out[0]["id"], json!(1));
|
||||
@@ -184,13 +228,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn notification_no_response() {
|
||||
let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"method\":\"echo\",\"params\":{}}\n");
|
||||
let out = run_once(
|
||||
echo_handlers(),
|
||||
"{\"jsonrpc\":\"2.0\",\"method\":\"echo\",\"params\":{}}\n",
|
||||
);
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_method() {
|
||||
let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"nope\"}\n");
|
||||
let out = run_once(
|
||||
echo_handlers(),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"nope\"}\n",
|
||||
);
|
||||
assert_eq!(out[0]["error"]["code"], json!(JSONRPC_METHOD_NOT_FOUND));
|
||||
}
|
||||
|
||||
@@ -209,13 +259,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn invalid_params_shape() {
|
||||
let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":42}\n");
|
||||
let out = run_once(
|
||||
echo_handlers(),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"echo\",\"params\":42}\n",
|
||||
);
|
||||
assert_eq!(out[0]["error"]["code"], json!(JSONRPC_INVALID_PARAMS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_error_passes_code() {
|
||||
let out = run_once(echo_handlers(), "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"bad_params\"}\n");
|
||||
let out = run_once(
|
||||
echo_handlers(),
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"bad_params\"}\n",
|
||||
);
|
||||
assert_eq!(out[0]["error"]["code"], json!(JSONRPC_INVALID_PARAMS));
|
||||
assert_eq!(out[0]["error"]["message"], json!("nope"));
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@ const PLACEHOLDER_LABEL: &str = "Loading…";
|
||||
/// cannot touch FLTK widgets.
|
||||
#[derive(Clone, Debug)]
|
||||
enum WorkerMsg {
|
||||
HubsLoaded(Result<Vec<Hub>, RpcError>),
|
||||
ProjectsLoaded(Result<Vec<Project>, RpcError>),
|
||||
TopFoldersLoaded(Result<Vec<Entry>, RpcError>),
|
||||
FolderContentsLoaded(String, Result<Vec<Entry>, RpcError>),
|
||||
Hubs(Result<Vec<Hub>, RpcError>),
|
||||
Projects(Result<Vec<Project>, RpcError>),
|
||||
TopFolders(Result<Vec<Entry>, RpcError>),
|
||||
FolderContents(String, Result<Vec<Entry>, RpcError>),
|
||||
}
|
||||
|
||||
struct State {
|
||||
@@ -198,7 +198,6 @@ impl BrowseDialog {
|
||||
sign_in.set_callback({
|
||||
let aps = aps.clone();
|
||||
let auth = auth.clone();
|
||||
let worker_tx = worker_tx;
|
||||
let mut status = status_mut.clone();
|
||||
move |_| {
|
||||
status.set_label("Signing in to Autodesk…");
|
||||
@@ -208,7 +207,7 @@ impl BrowseDialog {
|
||||
let outcome = auth
|
||||
.login_interactive(noop_auth())
|
||||
.and_then(|_| aps.list_hubs());
|
||||
worker_tx.send(WorkerMsg::HubsLoaded(outcome));
|
||||
worker_tx.send(WorkerMsg::Hubs(outcome));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -216,7 +215,6 @@ impl BrowseDialog {
|
||||
hub_combo.set_callback({
|
||||
let aps = aps.clone();
|
||||
let state = state.clone();
|
||||
let worker_tx = worker_tx;
|
||||
let mut projects_browser = projects_browser.clone();
|
||||
let mut tree = tree.clone();
|
||||
let mut status = status_mut.clone();
|
||||
@@ -247,7 +245,7 @@ impl BrowseDialog {
|
||||
status.set_label(&format!("Loading projects in {hub_name}…"));
|
||||
let aps = aps.clone();
|
||||
thread::spawn(move || {
|
||||
worker_tx.send(WorkerMsg::ProjectsLoaded(aps.list_projects(&hub_id)));
|
||||
worker_tx.send(WorkerMsg::Projects(aps.list_projects(&hub_id)));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -255,7 +253,6 @@ impl BrowseDialog {
|
||||
projects_browser.clone().set_callback({
|
||||
let aps = aps.clone();
|
||||
let state = state.clone();
|
||||
let worker_tx = worker_tx;
|
||||
let mut tree = tree.clone();
|
||||
let mut status = status_mut.clone();
|
||||
let action_btn_clone = action_btn.clone();
|
||||
@@ -286,8 +283,9 @@ impl BrowseDialog {
|
||||
status.set_label(&format!("Loading top folders in {project_name}…"));
|
||||
let aps = aps.clone();
|
||||
thread::spawn(move || {
|
||||
worker_tx
|
||||
.send(WorkerMsg::TopFoldersLoaded(aps.list_top_folders(&hub_id, &project_id)));
|
||||
worker_tx.send(WorkerMsg::TopFolders(
|
||||
aps.list_top_folders(&hub_id, &project_id),
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -295,7 +293,6 @@ impl BrowseDialog {
|
||||
tree.clone().set_callback({
|
||||
let aps = aps.clone();
|
||||
let state = state.clone();
|
||||
let worker_tx = worker_tx;
|
||||
let mut status = status_mut.clone();
|
||||
let action_btn_clone = action_btn.clone();
|
||||
move |t| match t.callback_reason() {
|
||||
@@ -306,10 +303,16 @@ impl BrowseDialog {
|
||||
refresh_action_button(&mut action_btn_clone.clone(), &s, mode);
|
||||
}
|
||||
TreeReason::Opened => {
|
||||
let Some(item) = t.callback_item() else { return };
|
||||
let Ok(path) = t.item_pathname(&item) else { return };
|
||||
let Some(item) = t.callback_item() else {
|
||||
return;
|
||||
};
|
||||
let Ok(path) = t.item_pathname(&item) else {
|
||||
return;
|
||||
};
|
||||
let mut s = state.borrow_mut();
|
||||
let Some(entry) = s.tree_entries.get(&path).cloned() else { return };
|
||||
let Some(entry) = s.tree_entries.get(&path).cloned() else {
|
||||
return;
|
||||
};
|
||||
if entry.entry_type != EntryType::Folders {
|
||||
return;
|
||||
}
|
||||
@@ -334,8 +337,9 @@ impl BrowseDialog {
|
||||
let folder_id = entry.id.clone();
|
||||
let aps = aps.clone();
|
||||
thread::spawn(move || {
|
||||
let result = list_folder_contents_for_mode(&aps, &project_id, &folder_id, mode);
|
||||
worker_tx.send(WorkerMsg::FolderContentsLoaded(path, result));
|
||||
let result =
|
||||
list_folder_contents_for_mode(&aps, &project_id, &folder_id, mode);
|
||||
worker_tx.send(WorkerMsg::FolderContents(path, result));
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
@@ -373,7 +377,7 @@ impl BrowseDialog {
|
||||
if auth.get_token().ok().flatten().is_some() {
|
||||
let aps = aps.clone();
|
||||
thread::spawn(move || {
|
||||
worker_tx.send(WorkerMsg::HubsLoaded(aps.list_hubs()));
|
||||
worker_tx.send(WorkerMsg::Hubs(aps.list_hubs()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -416,15 +420,15 @@ fn apply_worker_msg(
|
||||
mode: Mode,
|
||||
) {
|
||||
match msg {
|
||||
WorkerMsg::HubsLoaded(Err(e)) => show_error("Sign In Failed", &e.message),
|
||||
WorkerMsg::HubsLoaded(Ok(hubs)) => {
|
||||
WorkerMsg::Hubs(Err(e)) => show_error("Sign In Failed", &e.message),
|
||||
WorkerMsg::Hubs(Ok(hubs)) => {
|
||||
let mut s = state.borrow_mut();
|
||||
s.hubs = hubs;
|
||||
refill_hub_combo(hub_combo, &s.hubs);
|
||||
status.set_label("Signed in. Select a hub.");
|
||||
}
|
||||
WorkerMsg::ProjectsLoaded(Err(e)) => show_error("Load Projects Failed", &e.message),
|
||||
WorkerMsg::ProjectsLoaded(Ok(projects)) => {
|
||||
WorkerMsg::Projects(Err(e)) => show_error("Load Projects Failed", &e.message),
|
||||
WorkerMsg::Projects(Ok(projects)) => {
|
||||
let mut s = state.borrow_mut();
|
||||
s.projects = projects;
|
||||
projects_browser.clear();
|
||||
@@ -437,8 +441,8 @@ fn apply_worker_msg(
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkerMsg::TopFoldersLoaded(Err(e)) => show_error("Load Project Failed", &e.message),
|
||||
WorkerMsg::TopFoldersLoaded(Ok(folders)) => {
|
||||
WorkerMsg::TopFolders(Err(e)) => show_error("Load Project Failed", &e.message),
|
||||
WorkerMsg::TopFolders(Ok(folders)) => {
|
||||
let mut s = state.borrow_mut();
|
||||
for entry in folders {
|
||||
insert_tree_entry(tree, &mut s.tree_entries, "", &entry);
|
||||
@@ -457,11 +461,11 @@ fn apply_worker_msg(
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkerMsg::FolderContentsLoaded(parent, Err(e)) => {
|
||||
WorkerMsg::FolderContents(parent, Err(e)) => {
|
||||
state.borrow_mut().loaded_folders.remove(&parent);
|
||||
show_error("Load Folder Failed", &e.message);
|
||||
}
|
||||
WorkerMsg::FolderContentsLoaded(parent, Ok(children)) => {
|
||||
WorkerMsg::FolderContents(parent, Ok(children)) => {
|
||||
let mut s = state.borrow_mut();
|
||||
for entry in &children {
|
||||
insert_tree_entry(tree, &mut s.tree_entries, &parent, entry);
|
||||
@@ -474,6 +478,9 @@ fn apply_worker_msg(
|
||||
|
||||
// ---- worker helpers -------------------------------------------------------
|
||||
|
||||
/// Per-mode predicate deciding which listed entries survive.
|
||||
type EntryFilter = Box<dyn Fn(&Entry) -> bool>;
|
||||
|
||||
fn list_folder_contents_for_mode(
|
||||
aps: &Arc<ApsClient>,
|
||||
project_id: &str,
|
||||
@@ -485,7 +492,7 @@ fn list_folder_contents_for_mode(
|
||||
} else {
|
||||
&["folders", "items"]
|
||||
};
|
||||
let filter_box: Option<Box<dyn Fn(&Entry) -> bool>> = match mode {
|
||||
let filter_box: Option<EntryFilter> = match mode {
|
||||
Mode::Ifcfed => Some(Box::new(|e: &Entry| {
|
||||
e.display_name.to_lowercase().ends_with(".ifcfed")
|
||||
})),
|
||||
@@ -517,7 +524,9 @@ fn insert_tree_entry(
|
||||
parent_canonical: &str,
|
||||
entry: &Entry,
|
||||
) -> String {
|
||||
let parent_add = parent_canonical.strip_prefix('/').unwrap_or(parent_canonical);
|
||||
let parent_add = parent_canonical
|
||||
.strip_prefix('/')
|
||||
.unwrap_or(parent_canonical);
|
||||
let label = if entry.display_name.is_empty() {
|
||||
entry.id.clone()
|
||||
} else {
|
||||
@@ -565,7 +574,9 @@ fn add_unique(tree: &mut Tree, parent_add: &str, label: &str) -> String {
|
||||
}
|
||||
|
||||
fn collect_selected_entries(tree: &Tree, tree_entries: &HashMap<String, Entry>) -> Vec<Entry> {
|
||||
let Some(items) = tree.get_selected_items() else { return Vec::new() };
|
||||
let Some(items) = tree.get_selected_items() else {
|
||||
return Vec::new();
|
||||
};
|
||||
items
|
||||
.into_iter()
|
||||
.filter_map(|item| tree.item_pathname(&item).ok())
|
||||
@@ -586,7 +597,10 @@ fn update_status_for_selection(status: &mut Frame, entries: &[Entry], mode: Mode
|
||||
status.set_label(&format!("Selected {kind}: {}", entry.display_name));
|
||||
}
|
||||
n => {
|
||||
let valid = entries.iter().filter(|e| is_valid_selection(mode, e)).count();
|
||||
let valid = entries
|
||||
.iter()
|
||||
.filter(|e| is_valid_selection(mode, e))
|
||||
.count();
|
||||
status.set_label(&format!("Selected {valid} of {n} items."));
|
||||
}
|
||||
}
|
||||
@@ -636,11 +650,29 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn is_valid_selection_modes() {
|
||||
assert!(is_valid_selection(Mode::Ifcfed, &entry("a.ifcfed", EntryType::Items)));
|
||||
assert!(!is_valid_selection(Mode::Ifcfed, &entry("a.ifc", EntryType::Items)));
|
||||
assert!(is_valid_selection(Mode::Model, &entry("a.ifc", EntryType::Items)));
|
||||
assert!(is_valid_selection(Mode::Model, &entry("a.rdb", EntryType::Items)));
|
||||
assert!(is_valid_selection(Mode::Destination, &entry("folder", EntryType::Folders)));
|
||||
assert!(!is_valid_selection(Mode::Destination, &entry("file.ifc", EntryType::Items)));
|
||||
assert!(is_valid_selection(
|
||||
Mode::Ifcfed,
|
||||
&entry("a.ifcfed", EntryType::Items)
|
||||
));
|
||||
assert!(!is_valid_selection(
|
||||
Mode::Ifcfed,
|
||||
&entry("a.ifc", EntryType::Items)
|
||||
));
|
||||
assert!(is_valid_selection(
|
||||
Mode::Model,
|
||||
&entry("a.ifc", EntryType::Items)
|
||||
));
|
||||
assert!(is_valid_selection(
|
||||
Mode::Model,
|
||||
&entry("a.rdb", EntryType::Items)
|
||||
));
|
||||
assert!(is_valid_selection(
|
||||
Mode::Destination,
|
||||
&entry("folder", EntryType::Folders)
|
||||
));
|
||||
assert!(!is_valid_selection(
|
||||
Mode::Destination,
|
||||
&entry("file.ifc", EntryType::Items)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
//! Settings dialog: APS client id + OAuth callback port + sign-out.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::rc::Rc;
|
||||
|
||||
use fltk::{
|
||||
app,
|
||||
button::Button,
|
||||
enums::Align,
|
||||
frame::Frame,
|
||||
group::Flex,
|
||||
input::Input,
|
||||
prelude::*,
|
||||
app, button::Button, enums::Align, frame::Frame, group::Flex, input::Input, prelude::*,
|
||||
window::Window,
|
||||
};
|
||||
|
||||
@@ -20,11 +14,11 @@ use crate::ui::dialogs::{center_on_screen, confirm, drain_after_close, show_erro
|
||||
use crate::ui::ensure_app;
|
||||
|
||||
pub struct SettingsDialog {
|
||||
pub on_reload: Arc<dyn Fn() -> Result<(), RpcError>>,
|
||||
pub on_reload: Rc<dyn Fn() -> Result<(), RpcError>>,
|
||||
}
|
||||
|
||||
impl SettingsDialog {
|
||||
pub fn new(on_reload: Arc<dyn Fn() -> Result<(), RpcError>>) -> Self {
|
||||
pub fn new(on_reload: Rc<dyn Fn() -> Result<(), RpcError>>) -> Self {
|
||||
Self { on_reload }
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@ fn spawn_stub(routes: Vec<(&'static str, &'static str, u16, String)>) -> StubSer
|
||||
if h == "\r\n" || h == "\n" {
|
||||
break;
|
||||
}
|
||||
if let Some(v) = h.strip_prefix("Content-Length: ").or_else(|| h.strip_prefix("content-length: ")) {
|
||||
if let Some(v) = h
|
||||
.strip_prefix("Content-Length: ")
|
||||
.or_else(|| h.strip_prefix("content-length: "))
|
||||
{
|
||||
content_length = v.trim().parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +75,9 @@ fn spawn_stub(routes: Vec<(&'static str, &'static str, u16, String)>) -> StubSer
|
||||
let method = parts.first().copied().unwrap_or("");
|
||||
let path = parts.get(1).copied().unwrap_or("/");
|
||||
|
||||
let idx = routes.iter().position(|(m, p, _, _)| *m == method && path.starts_with(p));
|
||||
let idx = routes
|
||||
.iter()
|
||||
.position(|(m, p, _, _)| *m == method && path.starts_with(p));
|
||||
let (status, body) = match idx {
|
||||
Some(i) => {
|
||||
let (_, _, status, body) = routes.remove(i);
|
||||
@@ -156,9 +161,15 @@ fn list_folder_contents_filters_ifcfed() {
|
||||
{"id": "i2", "type": "items", "attributes": {"displayName": "model.ifcfed"}}
|
||||
]
|
||||
}"#;
|
||||
let server = spawn_stub(vec![("GET", "/data/v1/projects/P/folders/F/contents", 200, body.into())]);
|
||||
let server = spawn_stub(vec![(
|
||||
"GET",
|
||||
"/data/v1/projects/P/folders/F/contents",
|
||||
200,
|
||||
body.into(),
|
||||
)]);
|
||||
let client = make_client(server.base_url.clone());
|
||||
let filter = |e: &bonsaiviewer_autodesk::aps::Entry| e.display_name.to_lowercase().ends_with(".ifcfed");
|
||||
let filter =
|
||||
|e: &bonsaiviewer_autodesk::aps::Entry| e.display_name.to_lowercase().ends_with(".ifcfed");
|
||||
let entries = client
|
||||
.list_folder_contents("P", "F", &["folders", "items"], Some(&filter))
|
||||
.unwrap();
|
||||
@@ -193,12 +204,20 @@ fn get_item_returns_tip_metadata() {
|
||||
}
|
||||
}]
|
||||
}"#;
|
||||
let server = spawn_stub(vec![("GET", "/data/v1/projects/P/items/I", 200, body.into())]);
|
||||
let server = spawn_stub(vec![(
|
||||
"GET",
|
||||
"/data/v1/projects/P/items/I",
|
||||
200,
|
||||
body.into(),
|
||||
)]);
|
||||
let client = make_client(server.base_url.clone());
|
||||
let item = client.get_item("P", "I").unwrap();
|
||||
assert!(!item.hidden);
|
||||
assert_eq!(item.version_id.as_deref(), Some("V"));
|
||||
assert_eq!(item.storage_id.as_deref(), Some("urn:adsk.objects:os.object:bk/obj"));
|
||||
assert_eq!(
|
||||
item.storage_id.as_deref(),
|
||||
Some("urn:adsk.objects:os.object:bk/obj")
|
||||
);
|
||||
assert_eq!(item.last_modified_user_name.as_deref(), Some("alice"));
|
||||
assert_eq!(item.parent_folder_id.as_deref(), Some("F"));
|
||||
}
|
||||
@@ -216,7 +235,12 @@ fn get_item_handles_missing_tip_as_hidden() {
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let server = spawn_stub(vec![("GET", "/data/v1/projects/P/items/I", 200, body.into())]);
|
||||
let server = spawn_stub(vec![(
|
||||
"GET",
|
||||
"/data/v1/projects/P/items/I",
|
||||
200,
|
||||
body.into(),
|
||||
)]);
|
||||
let client = make_client(server.base_url.clone());
|
||||
let item = client.get_item("P", "I").unwrap();
|
||||
assert!(item.hidden);
|
||||
@@ -228,10 +252,7 @@ fn get_item_handles_missing_tip_as_hidden() {
|
||||
fn download_signed_then_get_writes_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dst = tmp.path().join("out.bin");
|
||||
let signed = format!(
|
||||
r#"{{"url": "{base}/payload"}}"#,
|
||||
base = "PLACEHOLDER"
|
||||
);
|
||||
let signed = format!(r#"{{"url": "{base}/payload"}}"#, base = "PLACEHOLDER");
|
||||
// We need two-stage: first signed url returns a pointer into the same
|
||||
// stub, then the GET against that path returns the bytes.
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
@@ -240,8 +261,18 @@ fn download_signed_then_get_writes_file() {
|
||||
let signed_with_url = signed.replace("PLACEHOLDER", &base);
|
||||
let payload_bytes = b"hello-bytes-12345";
|
||||
let mut routes: Vec<(&'static str, &'static str, u16, String)> = vec![
|
||||
("GET", "/oss/v2/buckets/bk/objects/obj/signeds3download", 200, signed_with_url),
|
||||
("GET", "/payload", 200, String::from_utf8(payload_bytes.to_vec()).unwrap()),
|
||||
(
|
||||
"GET",
|
||||
"/oss/v2/buckets/bk/objects/obj/signeds3download",
|
||||
200,
|
||||
signed_with_url,
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/payload",
|
||||
200,
|
||||
String::from_utf8(payload_bytes.to_vec()).unwrap(),
|
||||
),
|
||||
];
|
||||
// Hand-roll a stub on the bound listener so we can re-use the same port.
|
||||
let handle = thread::spawn(move || {
|
||||
@@ -262,7 +293,10 @@ fn download_signed_then_get_writes_file() {
|
||||
let parts: Vec<&str> = request_line.split_whitespace().collect();
|
||||
let method = parts[0];
|
||||
let path = parts[1];
|
||||
let idx = routes.iter().position(|(m, p, _, _)| *m == method && path.starts_with(p)).unwrap();
|
||||
let idx = routes
|
||||
.iter()
|
||||
.position(|(m, p, _, _)| *m == method && path.starts_with(p))
|
||||
.unwrap();
|
||||
let (_, _, status, body) = routes.remove(idx);
|
||||
let header = format!(
|
||||
"HTTP/1.1 {status} OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
@@ -298,10 +332,12 @@ fn upload_small_file_creates_new_item() {
|
||||
let base = format!("http://127.0.0.1:{port}");
|
||||
let presigned_url = format!("{base}/presigned/0");
|
||||
|
||||
let put_bytes_received: Arc<std::sync::Mutex<Vec<u8>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let put_bytes_received: Arc<std::sync::Mutex<Vec<u8>>> =
|
||||
Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let put_bytes_clone = put_bytes_received.clone();
|
||||
|
||||
let create_storage_body = r#"{"data":{"id":"urn:adsk.objects:os.object:bk/obj","type":"objects"}}"#.to_string();
|
||||
let create_storage_body =
|
||||
r#"{"data":{"id":"urn:adsk.objects:os.object:bk/obj","type":"objects"}}"#.to_string();
|
||||
let signed_upload_body = format!(r#"{{"uploadKey":"UK1","urls":["{presigned_url}"]}}"#);
|
||||
let complete_body = r#"{}"#.to_string();
|
||||
let folder_contents_body = r#"{"data":[]}"#.to_string();
|
||||
@@ -316,10 +352,22 @@ fn upload_small_file_creates_new_item() {
|
||||
let handle = thread::spawn(move || {
|
||||
let expected = vec![
|
||||
("POST", "/data/v1/projects/P/storage", create_storage_body),
|
||||
("GET", "/oss/v2/buckets/bk/objects/obj/signeds3upload", signed_upload_body),
|
||||
(
|
||||
"GET",
|
||||
"/oss/v2/buckets/bk/objects/obj/signeds3upload",
|
||||
signed_upload_body,
|
||||
),
|
||||
("PUT", "/presigned/0", String::new()),
|
||||
("POST", "/oss/v2/buckets/bk/objects/obj/signeds3upload", complete_body),
|
||||
("GET", "/data/v1/projects/P/folders/F/contents", folder_contents_body),
|
||||
(
|
||||
"POST",
|
||||
"/oss/v2/buckets/bk/objects/obj/signeds3upload",
|
||||
complete_body,
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/data/v1/projects/P/folders/F/contents",
|
||||
folder_contents_body,
|
||||
),
|
||||
("POST", "/data/v1/projects/P/items", create_item_body),
|
||||
];
|
||||
for (method, prefix, body) in expected {
|
||||
@@ -350,7 +398,11 @@ fn upload_small_file_creates_new_item() {
|
||||
}
|
||||
let parts: Vec<&str> = request_line.split_whitespace().collect();
|
||||
assert_eq!(parts[0], method, "request_line={request_line:?}");
|
||||
assert!(parts[1].starts_with(prefix), "got {} expected prefix {prefix}", parts[1]);
|
||||
assert!(
|
||||
parts[1].starts_with(prefix),
|
||||
"got {} expected prefix {prefix}",
|
||||
parts[1]
|
||||
);
|
||||
let header = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
|
||||
@@ -10,14 +10,26 @@ use serde_json::Value;
|
||||
fn binary_path() -> std::path::PathBuf {
|
||||
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("target");
|
||||
path.push(if cfg!(debug_assertions) { "debug" } else { "release" });
|
||||
path.push(if cfg!(windows) { "bonsaiviewer-autodesk.exe" } else { "bonsaiviewer-autodesk" });
|
||||
path.push(if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"release"
|
||||
});
|
||||
path.push(if cfg!(windows) {
|
||||
"bonsaiviewer-autodesk.exe"
|
||||
} else {
|
||||
"bonsaiviewer-autodesk"
|
||||
});
|
||||
path
|
||||
}
|
||||
|
||||
fn exchange(requests: &[&str]) -> Vec<Value> {
|
||||
let path = binary_path();
|
||||
assert!(path.exists(), "expected binary at {}; build with `cargo build --bin bonsaiviewer-autodesk` first", path.display());
|
||||
assert!(
|
||||
path.exists(),
|
||||
"expected binary at {}; build with `cargo build --bin bonsaiviewer-autodesk` first",
|
||||
path.display()
|
||||
);
|
||||
let mut child = Command::new(&path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
||||
Reference in New Issue
Block a user