TutorialVault: complete Tauri v2 port with runtime fixes
Rename from TutorialDock to TutorialVault. Remove legacy Python app and scripts. Fix video playback, subtitles, metadata display, window state persistence, and auto-download of ffmpeg/ffprobe on first run. Bundle fonts via npm instead of runtime download.
This commit is contained in:
@@ -26,6 +26,9 @@ use crate::AppPaths;
|
||||
/// Maximum chunk size for full-file responses (4 MB).
|
||||
const MAX_CHUNK: u64 = 4 * 1024 * 1024;
|
||||
|
||||
/// CORS header name.
|
||||
const CORS: &str = "Access-Control-Allow-Origin";
|
||||
|
||||
/// Register the `tutdock` custom protocol on the builder.
|
||||
pub fn register_protocol(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
builder.register_uri_scheme_protocol("tutdock", move |ctx, request| {
|
||||
@@ -140,6 +143,7 @@ fn serve_full(path: &PathBuf, file_size: u64, mime: &str) -> http::Response<Vec<
|
||||
.header(header::CONTENT_LENGTH, file_size.to_string())
|
||||
.header(header::ACCEPT_RANGES, "bytes")
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(CORS, "*")
|
||||
.body(buf)
|
||||
.unwrap_or_else(|_| internal_error("response build error"))
|
||||
}
|
||||
@@ -180,6 +184,7 @@ fn serve_range(
|
||||
.header(header::CONTENT_LENGTH, length.to_string())
|
||||
.header(header::ACCEPT_RANGES, "bytes")
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(CORS, "*")
|
||||
.body(buf)
|
||||
.unwrap_or_else(|_| internal_error("response build error"))
|
||||
}
|
||||
@@ -215,33 +220,36 @@ fn parse_range(header: &str, file_size: u64) -> Option<(u64, u64)> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn handle_subtitle(app: &tauri::AppHandle, rest: &str) -> http::Response<Vec<u8>> {
|
||||
// Path: sub/{libid}/{fid}
|
||||
let parts: Vec<&str> = rest.splitn(2, '/').collect();
|
||||
if parts.len() != 2 {
|
||||
return not_found();
|
||||
}
|
||||
let fid = parts[1];
|
||||
// rest is a VTT filename like "{fid}_{name}.vtt"
|
||||
let filename = rest;
|
||||
|
||||
if !fid.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
// Validate: no path traversal
|
||||
if filename.contains("..") || filename.contains('/') || filename.contains('\\') || filename.is_empty() {
|
||||
return not_found();
|
||||
}
|
||||
|
||||
let paths = app.state::<AppPaths>();
|
||||
let vtt_path = paths.subs_dir.join(format!("{}.vtt", fid));
|
||||
let vtt_path = paths.subs_dir.join(filename);
|
||||
|
||||
if !vtt_path.exists() {
|
||||
// Ensure resolved path is still inside subs_dir
|
||||
if let (Ok(base), Ok(resolved)) = (paths.subs_dir.canonicalize(), vtt_path.canonicalize()) {
|
||||
if !resolved.starts_with(&base) {
|
||||
return not_found();
|
||||
}
|
||||
} else {
|
||||
return not_found();
|
||||
}
|
||||
|
||||
let data = match fs::read(&vtt_path) {
|
||||
Ok(d) => d,
|
||||
Err(_) => return internal_error("read error"),
|
||||
Err(_) => return not_found(),
|
||||
};
|
||||
|
||||
http::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "text/vtt; charset=utf-8")
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(CORS, "*")
|
||||
.body(data)
|
||||
.unwrap_or_else(|_| internal_error("response build error"))
|
||||
}
|
||||
@@ -331,6 +339,7 @@ fn serve_static_file(base_dir: &PathBuf, filename: &str) -> http::Response<Vec<u
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(CORS, "*")
|
||||
.body(data)
|
||||
.unwrap_or_else(|_| internal_error("response build error"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user