Bound native media preview capabilities and source range reads
This commit is contained in:
1 parent
4b0b5ebfd9
commit
370db7e791
13 files changed
+834
-46
No files matched your search
@@ -7,6 +7,7 @@ mod ai_commands;
|
||||
mod appearance;
|
||||
mod data_directory;
|
||||
mod instance;
|
||||
mod media_preview;
|
||||
mod model_download;
|
||||
mod semantic_commands;
|
||||
mod settings;
|
||||
@@ -44,6 +45,7 @@ struct AppState {
|
||||
semantic: Arc<SemanticState>,
|
||||
watching: Arc<watching::WatchService>,
|
||||
index_options: searchi_core::IndexOptions,
|
||||
previews: Arc<media_preview::PreviewRegistry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -356,17 +358,20 @@ struct MediaPreview {
|
||||
kind: &'static str,
|
||||
}
|
||||
|
||||
// The asset protocol starts with an empty scope. Only a validated indexed
|
||||
// media file is granted access; the webview receives no general filesystem API.
|
||||
// Return a bounded opaque capability, never a lifetime asset-scope permission.
|
||||
#[tauri::command]
|
||||
async fn prepare_media_preview(
|
||||
path: String,
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<MediaPreview, String> {
|
||||
let database = state.database.clone();
|
||||
let previews = state.previews.clone();
|
||||
let admission = previews
|
||||
.admit()
|
||||
.ok_or("Media preview is busy; try again.")?;
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let index = SearchIndex::open(database).map_err(|error| error.to_string())?;
|
||||
let _admission = admission;
|
||||
let index = SearchIndex::open(&database).map_err(|error| error.to_string())?;
|
||||
let canonical = validated_result(&index, &path)?;
|
||||
let kind = match searchi_media::classify(&canonical) {
|
||||
Some(searchi_media::MediaKind::Image) => "image",
|
||||
@@ -374,33 +379,8 @@ async fn prepare_media_preview(
|
||||
Some(searchi_media::MediaKind::Video) => "video",
|
||||
None => return Err("This file is not a supported media preview.".into()),
|
||||
};
|
||||
if kind == "image" {
|
||||
let bytes = std::fs::metadata(&canonical)
|
||||
.map_err(|error| error.to_string())?
|
||||
.len();
|
||||
if bytes > 64 * 1024 * 1024 {
|
||||
return Err("Image exceeds the 64 MiB preview limit.".into());
|
||||
}
|
||||
let reader = image::ImageReader::open(&canonical)
|
||||
.map_err(|error| error.to_string())?
|
||||
.with_guessed_format()
|
||||
.map_err(|error| error.to_string())?;
|
||||
// Resource limits follow decoded format, not a renameable suffix.
|
||||
let max_pixels = preview_pixel_limit(reader.format());
|
||||
let (width, height) = reader
|
||||
.into_dimensions()
|
||||
.map_err(|error| format!("Image preview is unavailable: {error}"))?;
|
||||
if width == 0 || height == 0 || u64::from(width) * u64::from(height) > max_pixels {
|
||||
return Err("Image exceeds the supported preview dimensions.".into());
|
||||
}
|
||||
}
|
||||
app.asset_protocol_scope()
|
||||
.allow_file(&canonical)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(MediaPreview {
|
||||
path: canonical.to_string_lossy().into_owned(),
|
||||
kind,
|
||||
})
|
||||
let token = previews.grant(database, canonical, kind)?;
|
||||
Ok(MediaPreview { path: token, kind })
|
||||
})
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
@@ -554,11 +534,24 @@ fn main() {
|
||||
std::env::var_os("SEARCHI_DATA_DIR"),
|
||||
)
|
||||
.expect("searchi profile identity could not be resolved");
|
||||
let previews = Arc::new(media_preview::PreviewRegistry::default());
|
||||
let protocol_previews = previews.clone();
|
||||
tauri::Builder::default()
|
||||
// Detect duplicates before any model, database, watcher or other plugin starts.
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
instance::focus_search(app);
|
||||
}))
|
||||
.register_asynchronous_uri_scheme_protocol("searchi-media", move |_context, request, responder| {
|
||||
let Some(admission) = protocol_previews.admit() else {
|
||||
responder.respond(media_preview::error_response(tauri::http::StatusCode::SERVICE_UNAVAILABLE));
|
||||
return;
|
||||
};
|
||||
let registry = protocol_previews.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let _admission = admission;
|
||||
responder.respond(registry.respond(request));
|
||||
});
|
||||
})
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().with_handler(|app, _, event| {
|
||||
@@ -566,7 +559,7 @@ fn main() {
|
||||
instance::focus_search(app);
|
||||
}
|
||||
}).build())
|
||||
.setup(|app| {
|
||||
.setup(move |app| {
|
||||
let directory = data_directory::resolve(
|
||||
app.path().app_data_dir()?,
|
||||
std::env::var_os("SEARCHI_DATA_DIR"),
|
||||
@@ -673,6 +666,7 @@ fn main() {
|
||||
semantic,
|
||||
watching,
|
||||
index_options,
|
||||
previews: previews.clone(),
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user