Skip to content

Commit fd28a5f

Browse files
authored
Improved Error Handling on Export (#476)
* Improved Error Handling on Export While this doesn't directly address #474 as am unable to reproduce the error. It should help in diagnosing what's happening and protect from some crashes by reducing the unwraps() in the backend code. * fix typos
1 parent f387d59 commit fd28a5f

10 files changed

Lines changed: 205 additions & 186 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ time = "0.3.41"
3737
tauri-plugin-dialog = "2"
3838
tauri-plugin-notification = "2"
3939
tauri-plugin-log = "2"
40+
once_cell = "1.17.2"
4041

4142
[target.'cfg(target_os = "windows")'.dependencies.ort]
4243
version = "2.0.0-rc.9"

src-tauri/gen/schemas/acl-manifests.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src-tauri/gen/schemas/desktop-schema.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
],
3838
"definitions": {
3939
"Capability": {
40-
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
40+
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
4141
"type": "object",
4242
"required": [
4343
"identifier",
@@ -49,7 +49,7 @@
4949
"type": "string"
5050
},
5151
"description": {
52-
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
52+
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
5353
"default": "",
5454
"type": "string"
5555
},
@@ -1016,6 +1016,12 @@
10161016
"const": "core:webview:allow-reparent",
10171017
"markdownDescription": "Enables the reparent command without any pre-configured scope."
10181018
},
1019+
{
1020+
"description": "Enables the set_webview_auto_resize command without any pre-configured scope.",
1021+
"type": "string",
1022+
"const": "core:webview:allow-set-webview-auto-resize",
1023+
"markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope."
1024+
},
10191025
{
10201026
"description": "Enables the set_webview_background_color command without any pre-configured scope.",
10211027
"type": "string",
@@ -1118,6 +1124,12 @@
11181124
"const": "core:webview:deny-reparent",
11191125
"markdownDescription": "Denies the reparent command without any pre-configured scope."
11201126
},
1127+
{
1128+
"description": "Denies the set_webview_auto_resize command without any pre-configured scope.",
1129+
"type": "string",
1130+
"const": "core:webview:deny-set-webview-auto-resize",
1131+
"markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope."
1132+
},
11211133
{
11221134
"description": "Denies the set_webview_background_color command without any pre-configured scope.",
11231135
"type": "string",

src-tauri/gen/schemas/macOS-schema.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
],
3838
"definitions": {
3939
"Capability": {
40-
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
40+
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
4141
"type": "object",
4242
"required": [
4343
"identifier",
@@ -49,7 +49,7 @@
4949
"type": "string"
5050
},
5151
"description": {
52-
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
52+
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
5353
"default": "",
5454
"type": "string"
5555
},
@@ -1016,6 +1016,12 @@
10161016
"const": "core:webview:allow-reparent",
10171017
"markdownDescription": "Enables the reparent command without any pre-configured scope."
10181018
},
1019+
{
1020+
"description": "Enables the set_webview_auto_resize command without any pre-configured scope.",
1021+
"type": "string",
1022+
"const": "core:webview:allow-set-webview-auto-resize",
1023+
"markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope."
1024+
},
10191025
{
10201026
"description": "Enables the set_webview_background_color command without any pre-configured scope.",
10211027
"type": "string",
@@ -1118,6 +1124,12 @@
11181124
"const": "core:webview:deny-reparent",
11191125
"markdownDescription": "Denies the reparent command without any pre-configured scope."
11201126
},
1127+
{
1128+
"description": "Denies the set_webview_auto_resize command without any pre-configured scope.",
1129+
"type": "string",
1130+
"const": "core:webview:deny-set-webview-auto-resize",
1131+
"markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope."
1132+
},
11211133
{
11221134
"description": "Denies the set_webview_background_color command without any pre-configured scope.",
11231135
"type": "string",

src-tauri/src/lib.rs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,3 @@ pub mod megadetector;
33
pub mod structures;
44
pub mod util;
55
pub mod yolov5;
6-
7-
// #[cfg(test)]
8-
// mod tests {
9-
// use super::yolov5::helpers::render_detections;
10-
// use super::yolov5::YoloModel;
11-
12-
// #[test]
13-
// fn create_model() {
14-
// let image_path = "tests/fixtures/dataset/IMG_0089_peccary.JPG";
15-
16-
// let model = YoloModel::new_from_file("../md_v5a.0.0-dynamic.onnx", (1280, 1280));
17-
// assert!(model.is_ok());
18-
19-
// let mut model = model.unwrap();
20-
21-
// let detections = model.detect(image_path, 0.1, 0.45);
22-
23-
// assert!(detections.is_ok());
24-
25-
// let detections = detections.unwrap();
26-
27-
// assert_eq!(detections.detections.len(), 1);
28-
29-
// render_detections(image_path, &detections, "output.jpg").unwrap();
30-
// }
31-
// }

src-tauri/src/main.rs

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,38 @@ fn export_csv(
4747
results: Vec<structures::CamTrapImageDetections>,
4848
output_path: PathBuf,
4949
) -> Result<(), String> {
50-
let mut writer = csv::Writer::from_path(output_path).unwrap();
50+
let mut writer = csv::Writer::from_path(&output_path)
51+
.map_err(|e| format!("Failed to create CSV writer: {}", e))?;
5152

5253
for result in results {
53-
if let Some(error) = result.error {
54-
writer
55-
.serialize(CamTrapCSVDetection::new_error(result.file, error))
56-
.unwrap();
54+
let res = if let Some(error) = result.error {
55+
writer.serialize(CamTrapCSVDetection::new_error(result.file, error))
5756
} else if result.detections.is_empty() {
58-
writer
59-
.serialize(CamTrapCSVDetection::new_empty(result.file))
60-
.unwrap();
57+
writer.serialize(CamTrapCSVDetection::new_empty(result.file))
6158
} else {
59+
let mut row_result = Ok(());
6260
for detection in result.detections {
63-
writer
64-
.serialize(CamTrapCSVDetection::new_detection(
65-
result.file.clone(),
66-
result.image_width.unwrap(),
67-
result.image_height.unwrap(),
68-
&detection,
69-
))
70-
.unwrap();
61+
if let Err(e) = writer.serialize(CamTrapCSVDetection::new_detection(
62+
result.file.clone(),
63+
result.image_width.unwrap_or(0),
64+
result.image_height.unwrap_or(0),
65+
&detection,
66+
)) {
67+
row_result = Err(e);
68+
break;
69+
}
7170
}
71+
row_result
72+
};
73+
74+
if let Err(e) = res {
75+
return Err(format!("Failed to write CSV row: {}", e));
7276
}
7377
}
7478

75-
writer.flush().unwrap();
79+
writer
80+
.flush()
81+
.map_err(|e| format!("Failed to flush CSV writer: {}", e))?;
7682

7783
Ok(())
7884
}
@@ -81,12 +87,14 @@ fn export_json(
8187
results: Vec<structures::CamTrapImageDetections>,
8288
output_path: PathBuf,
8389
) -> Result<(), String> {
84-
let mut writer = std::fs::File::create(output_path).unwrap();
90+
let mut writer = std::fs::File::create(&output_path)
91+
.map_err(|e| format!("Failed to create JSON file: {}", e))?;
8592
let json_images: Vec<exports::json::CamTrapJSONImageDetections> =
8693
results.into_iter().map(|d| d.into()).collect();
8794
let json_container = exports::json::CamTrapJSONContainer::new(json_images);
8895

89-
serde_json::to_writer_pretty(&mut writer, &json_container).unwrap();
96+
serde_json::to_writer_pretty(&mut writer, &json_container)
97+
.map_err(|e| format!("Failed to write JSON: {}", e))?;
9098

9199
Ok(())
92100
}
@@ -96,22 +104,14 @@ async fn export_image_set(
96104
state: tauri::State<'_, AppState>,
97105
output_path: PathBuf,
98106
filter_criteria: FilterCriteria,
99-
draw_criteria: DrawCriteria,
100-
window: Window,
101-
) -> Result<(), ()> {
107+
draw_criteria: DrawCriteria
108+
) -> Result<String, String> {
102109
let results = state.0.lock().unwrap().results.clone();
103110
let base_dir = state.0.lock().unwrap().base_dir.clone();
104111

105112
// Ensure it's not the same folder as the raw images
106113
if output_path == base_dir {
107-
window
108-
.dialog()
109-
.message("The export folder cannot be the same as the raw images folder.")
110-
.kind(MessageDialogKind::Error)
111-
.title("Export Error")
112-
.blocking_show();
113-
114-
return Err(());
114+
return Err("The export folder cannot be the same as the raw images folder.".to_string());
115115
}
116116

117117
export_image(
@@ -120,17 +120,9 @@ async fn export_image_set(
120120
output_path,
121121
filter_criteria,
122122
draw_criteria,
123-
)
124-
.unwrap();
123+
).map_err(|e| format!("Failed to export images: {}", e))?;
125124

126-
window
127-
.dialog()
128-
.message("The image export has completed.")
129-
.kind(MessageDialogKind::Info)
130-
.title("Image Export Complete")
131-
.blocking_show();
132-
133-
Ok(())
125+
Ok("The image export has completed.".to_string())
134126
}
135127

136128
#[tauri::command]
@@ -172,6 +164,16 @@ async fn export(
172164
_ => "Unknown",
173165
};
174166

167+
if let Err(err) = r {
168+
window
169+
.dialog()
170+
.message(format!("Failed to export {}: {}", format_name, err))
171+
.kind(MessageDialogKind::Error)
172+
.title("Export Failed")
173+
.blocking_show();
174+
return Err(err);
175+
}
176+
175177
window
176178
.dialog()
177179
.message(format!("The {} export has completed.", format_name))

src-tauri/src/yolov5/helpers.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,32 @@ use super::detections::YoloImageDetections;
22
use image::{ImageError, Rgb};
33
use imageproc::drawing::draw_hollow_rect_mut;
44
use imageproc::rect::Rect;
5+
use std::collections::HashSet;
56
use std::path::{Path, PathBuf};
7+
use once_cell::sync::Lazy;
68
use walkdir::WalkDir;
79

8-
const IMAGE_EXTENTIONS: [&str; 3] = ["jpg", "jpeg", "png"];
10+
/// A set of known image file extensions
11+
static IMAGE_EXTENSIONS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
12+
["jpg", "jpeg", "png"].iter().cloned().collect()
13+
});
914

10-
/// Check if path is a known image extention
15+
/// Check if path is a known image extension
1116
pub fn is_image_path(path: &Path) -> bool {
12-
match path.extension() {
13-
None => false,
14-
Some(a) => IMAGE_EXTENTIONS.contains(&a.to_str().unwrap().to_lowercase().as_str()),
15-
}
17+
path.extension()
18+
.and_then(|ext| ext.to_str())
19+
.map(|ext| IMAGE_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
20+
.unwrap_or(false)
1621
}
1722

18-
/// Find all images beleived to be an image.
23+
/// Find all images in a directory
24+
/// If `recursive` is true, it will search all subdirectories.
25+
/// If `recursive` is false, it will only search the root directory.
1926
pub fn enumerate_images(root_dir: PathBuf, recursive: bool) -> Vec<PathBuf> {
2027
let mut images: Vec<PathBuf> = vec![];
2128

2229
for entry in WalkDir::new(root_dir)
23-
.max_depth(if recursive { ::std::usize::MAX } else { 1 })
30+
.max_depth(if recursive { usize::MAX } else { 1 })
2431
.into_iter()
2532
.filter_map(|e| e.ok())
2633
.filter(|e| is_image_path(e.path()))

src/api.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface ProgressReport {
2222
}
2323

2424
export async function listenProgress(
25-
onProgress: (report: ProgressReport) => void,
25+
onProgress: (report: ProgressReport) => void
2626
) {
2727
return await listen("progress", (event) => {
2828
const report = event.payload as ProgressReport;
@@ -37,7 +37,7 @@ export async function process(
3737
path: string,
3838
confidenceThreshold: number,
3939
recursive: boolean,
40-
onProgress?: (report: ProgressReport) => void,
40+
onProgress?: (report: ProgressReport) => void
4141
) {
4242
await invoke("process", {
4343
path,
@@ -59,7 +59,10 @@ export type ExportFormat = "json" | "csv";
5959
export type ImageExportFormat = "image-dir";
6060
export type AllExportFormat = ExportFormat | ImageExportFormat;
6161

62-
export async function createExport(format: ExportFormat, outputPath: string) {
62+
export async function createExport(
63+
format: ExportFormat,
64+
outputPath: string
65+
) {
6366
return await invoke("export", { format, outputPath });
6467
}
6568

@@ -76,7 +79,7 @@ export function createFilterCriteria(
7679
animals: FilterCriteriaOption,
7780
humans: FilterCriteriaOption,
7881
vehicles: FilterCriteriaOption,
79-
empty: FilterCriteriaOption,
82+
empty: FilterCriteriaOption
8083
): FilterCriteria {
8184
return {
8285
animals: animals,
@@ -95,7 +98,7 @@ interface DrawCriteria {
9598
export function createDrawCriteria(
9699
animals: boolean,
97100
humans: boolean,
98-
vehicles: boolean,
101+
vehicles: boolean
99102
) {
100103
return {
101104
animals: animals,
@@ -107,7 +110,7 @@ export function createDrawCriteria(
107110
export function exportImageSet(
108111
outputPath: string,
109112
filterCriteria: FilterCriteria,
110-
drawCriteria: DrawCriteria,
113+
drawCriteria: DrawCriteria
111114
) {
112115
return invoke("export_image_set", {
113116
outputPath,

0 commit comments

Comments
 (0)