forked from 1b-agent/opencadstudio-upstream
Merge pull request #1351 from helenkwok/feat/web-open-from-bytes
Web control: open a drawing from bytes
This commit is contained in:
commit
9613e6edf9
3 files changed
+124
-19
No files matched your search
@@ -131,6 +131,24 @@ struct Operation {
|
||||
fn failure(code: &str, error: impl ToString) -> Value {
|
||||
json!({"ok":false,"status":"failed","code":code,"error":error.to_string()})
|
||||
}
|
||||
/// `{"op":"open","name":"plan.dwg","data_base64":"…"}`: the web build has no
|
||||
/// filesystem path to open, so the caller sends the file itself. `name` is
|
||||
/// reduced to its file name; it labels the tab and the recent-files entry.
|
||||
fn open_bytes_request(req: &Value) -> Result<(String, Vec<u8>), Value> {
|
||||
use base64::Engine as _;
|
||||
let name = string(req, "name")?;
|
||||
let name = name.rsplit(['/', '\\']).next().unwrap_or(name);
|
||||
if name.is_empty() {
|
||||
return Err(failure("invalid_request", "Missing name"));
|
||||
}
|
||||
let bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(req["data_base64"].as_str().unwrap_or_default())
|
||||
.map_err(|e| failure("invalid_request", format!("data_base64: {e}")))?;
|
||||
if bytes.is_empty() {
|
||||
return Err(failure("invalid_request", "data_base64 is empty"));
|
||||
}
|
||||
Ok((name.to_string(), bytes))
|
||||
}
|
||||
fn string<'a>(req: &'a Value, key: &str) -> Result<&'a str, Value> {
|
||||
req[key]
|
||||
.as_str()
|
||||
@@ -703,6 +721,24 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
Ok(match req["op"].as_str().unwrap_or("") {
|
||||
"new" => self.update(Message::TabNew),
|
||||
"open" if req.get("data_base64").is_some() => {
|
||||
let (name, bytes) = open_bytes_request(req)?;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if self.opening.is_some() {
|
||||
return Err(failure("busy", "Another drawing is still opening"));
|
||||
}
|
||||
self.open_web_bytes(name, bytes)
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let _ = (name, bytes);
|
||||
return Err(failure(
|
||||
"invalid_request",
|
||||
"data_base64 is for the web build; supply path",
|
||||
));
|
||||
}
|
||||
}
|
||||
"open" => self.update(Message::OpenExternal(std::path::PathBuf::from(string(
|
||||
req, "path",
|
||||
)?))),
|
||||
@@ -1409,6 +1445,41 @@ mod tests {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_from_bytes_validates_the_request() {
|
||||
let ok = open_bytes_request(&json!({"name":"C:\\plans\\plan.dwg","data_base64":"AAEC"}));
|
||||
assert_eq!(ok, Ok(("plan.dwg".to_string(), vec![0, 1, 2])));
|
||||
for (req, error) in [
|
||||
(json!({"data_base64":"AAEC"}), "Missing name"),
|
||||
(json!({"name":"dir/","data_base64":"AAEC"}), "Missing name"),
|
||||
(
|
||||
json!({"name":"a.dwg","data_base64":""}),
|
||||
"data_base64 is empty",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
open_bytes_request(&req).unwrap_err()["error"],
|
||||
error,
|
||||
"{req}"
|
||||
);
|
||||
}
|
||||
let bad = open_bytes_request(&json!({"name":"a.dwg","data_base64":"not base64!"}));
|
||||
assert_eq!(bad.unwrap_err()["code"], "invalid_request");
|
||||
}
|
||||
#[test]
|
||||
fn open_from_bytes_is_refused_natively_without_touching_documents() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
request(&mut app, json!({"op":"new"}));
|
||||
let tabs = app.tabs.len();
|
||||
let reply = request(
|
||||
&mut app,
|
||||
json!({"op":"open","name":"a.dxf","data_base64":"AAEC"}),
|
||||
);
|
||||
assert_eq!(reply["code"], "invalid_request", "{reply}");
|
||||
assert_eq!(app.tabs.len(), tabs);
|
||||
assert!(app.opening.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_image_op_places_an_ole2frame_and_undoes_it() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
+41
-19
@@ -1182,31 +1182,53 @@ impl OpenCADStudio {
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
// `FileOpened` only installs the result when an open is in
|
||||
// progress, so mark one. The browser picker + parse happen
|
||||
// inside `pick_and_load_web`; the real name is unknown until
|
||||
// then, so show a generic label meanwhile.
|
||||
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
||||
crate::app::OPEN_PHASE_READING,
|
||||
));
|
||||
let open_id = self.next_open_id();
|
||||
self.opening = Some(crate::app::OpenProgress {
|
||||
id: open_id,
|
||||
name: "Opening…".into(),
|
||||
source_path: None,
|
||||
size_bytes: 0,
|
||||
state: state.clone(),
|
||||
started: Instant::now(),
|
||||
recovery_error: None,
|
||||
recovery_read_stats: None,
|
||||
recovery_bytes: None,
|
||||
});
|
||||
// The browser picker + parse happen inside `pick_and_load_web`;
|
||||
// the real name is unknown until then, so show a generic label
|
||||
// meanwhile.
|
||||
let (open_id, state) = self.begin_web_open("Opening…".into(), 0);
|
||||
Task::perform(crate::io::pick_and_load_web(state), move |outcome| {
|
||||
Message::WebFileOpened(open_id, outcome)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Web: open a drawing from bytes the caller already holds, through the
|
||||
/// same path as a file chosen in the browser picker.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(in crate::app) fn open_web_bytes(&mut self, name: String, bytes: Vec<u8>) -> Task<Message> {
|
||||
let (open_id, state) = self.begin_web_open(name.clone(), bytes.len() as u64);
|
||||
Task::perform(
|
||||
crate::io::open_bytes_web(name, std::sync::Arc::from(bytes), state),
|
||||
move |outcome| Message::WebFileOpened(open_id, outcome),
|
||||
)
|
||||
}
|
||||
|
||||
/// `FileOpened` only installs the result when an open is in progress, so
|
||||
/// mark one.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn begin_web_open(
|
||||
&mut self,
|
||||
name: String,
|
||||
size_bytes: u64,
|
||||
) -> (u64, std::sync::Arc<crate::io::OpenProgressState>) {
|
||||
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
||||
crate::app::OPEN_PHASE_READING,
|
||||
));
|
||||
let open_id = self.next_open_id();
|
||||
self.opening = Some(crate::app::OpenProgress {
|
||||
id: open_id,
|
||||
name,
|
||||
source_path: None,
|
||||
size_bytes,
|
||||
state: state.clone(),
|
||||
started: Instant::now(),
|
||||
recovery_error: None,
|
||||
recovery_read_stats: None,
|
||||
recovery_bytes: None,
|
||||
});
|
||||
(open_id, state)
|
||||
}
|
||||
|
||||
pub(in crate::app) fn next_open_id(&mut self) -> u64 {
|
||||
self.open_job_serial = self.open_job_serial.wrapping_add(1).max(1);
|
||||
self.open_job_serial
|
||||
|
||||
@@ -526,6 +526,18 @@ pub async fn pick_and_load_web(
|
||||
let name = handle.file_name();
|
||||
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||
let bytes: Arc<[u8]> = Arc::from(handle.read().await);
|
||||
open_bytes_web(name, bytes, progress).await
|
||||
}
|
||||
|
||||
/// Web file open from bytes the caller already holds (the control channel's
|
||||
/// `open` with `data_base64`). Same parse, caching and recovery handling as a
|
||||
/// file chosen in the browser picker.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open_bytes_web(
|
||||
name: String,
|
||||
bytes: Arc<[u8]>,
|
||||
progress: Arc<OpenProgressState>,
|
||||
) -> WebOpenOutcome {
|
||||
let size_bytes = bytes.len() as u64;
|
||||
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
|
||||
let keep_for_recovery = result
|
||||
|
||||
Reference in new issue
Block a user