feat: refine download UX and native host flow

This commit is contained in:
tongki078
2026-02-25 11:12:23 +09:00
parent 34f63acf49
commit d85fdc1101
12 changed files with 474 additions and 72 deletions

2
src-tauri/Cargo.lock generated
View File

@@ -77,7 +77,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "app"
version = "0.1.0"
version = "0.1.2"
dependencies = [
"base64 0.22.1",
"log",

View File

@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.1.1"
version = "0.1.2"
description = "A Tauri App"
authors = ["you"]
license = ""

View File

@@ -1,7 +1,9 @@
mod engine;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use tauri::Manager;
@@ -46,6 +48,145 @@ struct ExternalAddRequest {
split: Option<u32>,
}
fn default_extension_ids() -> Vec<String> {
vec![
"alaohbbicffclloghmknhlmfdbobcigc".to_string(),
"makoclohjdpempbndoaljeadpngefhcf".to_string(),
]
}
fn collect_extension_ids() -> Vec<String> {
let mut ids = default_extension_ids();
for key in ["GDOWN_EXTENSION_ID", "GDOWN_EXTENSION_IDS"] {
if let Ok(raw) = env::var(key) {
for id in raw.split([',', ' ', '\n', '\t']) {
let trimmed = id.trim();
if trimmed.is_empty() {
continue;
}
if !ids.iter().any(|v| v == trimmed) {
ids.push(trimmed.to_string());
}
}
}
}
ids
}
#[cfg(target_os = "macos")]
fn merge_existing_allowed_origins(manifest_path: &Path, ids: &mut Vec<String>) {
let content = match fs::read_to_string(manifest_path) {
Ok(v) => v,
Err(_) => return,
};
let parsed: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(_) => return,
};
let Some(origins) = parsed.get("allowed_origins").and_then(|v| v.as_array()) else {
return;
};
for origin in origins {
let Some(value) = origin.as_str() else {
continue;
};
let prefix = "chrome-extension://";
if !value.starts_with(prefix) {
continue;
}
let id = value.trim_start_matches(prefix).trim_end_matches('/');
if id.is_empty() {
continue;
}
if !ids.iter().any(|v| v == id) {
ids.push(id.to_string());
}
}
}
fn resolve_node_path() -> Option<PathBuf> {
if let Ok(path_var) = env::var("PATH") {
for dir in env::split_paths(&path_var) {
let node = dir.join("node");
if node.is_file() {
return Some(node);
}
}
}
for candidate in ["/usr/local/bin/node", "/opt/homebrew/bin/node", "/usr/bin/node"] {
let path = PathBuf::from(candidate);
if path.is_file() {
return Some(path);
}
}
None
}
#[cfg(target_os = "macos")]
fn ensure_native_host_installed() -> Result<PathBuf, String> {
let host_name = "org.gdown.nativehost";
let home = env::var("HOME").map_err(|err| format!("HOME 경로 확인 실패: {err}"))?;
let tools_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("tools")
.join("native-host");
let host_script = tools_dir.join("host.mjs");
if !host_script.is_file() {
return Err(format!("native host script not found: {}", host_script.display()));
}
let node_path = resolve_node_path().ok_or_else(|| "node 경로를 찾을 수 없습니다.".to_string())?;
let runtime_dir = tools_dir.join(".runtime");
fs::create_dir_all(&runtime_dir).map_err(|err| format!("runtime 디렉터리 생성 실패: {err}"))?;
let runner_path = runtime_dir.join("run-host-macos.sh");
let runner_content = format!(
"#!/usr/bin/env bash\nset -euo pipefail\nexec \"{}\" \"{}\"\n",
node_path.display(),
host_script.display()
);
fs::write(&runner_path, runner_content).map_err(|err| format!("runner 생성 실패: {err}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perm = fs::Permissions::from_mode(0o755);
fs::set_permissions(&runner_path, perm).map_err(|err| format!("runner 권한 설정 실패: {err}"))?;
}
let chrome_hosts_dir = Path::new(&home)
.join("Library")
.join("Application Support")
.join("Google")
.join("Chrome")
.join("NativeMessagingHosts");
fs::create_dir_all(&chrome_hosts_dir).map_err(|err| format!("native host 디렉터리 생성 실패: {err}"))?;
let manifest_path = chrome_hosts_dir.join(format!("{host_name}.json"));
let mut extension_ids = collect_extension_ids();
merge_existing_allowed_origins(&manifest_path, &mut extension_ids);
let allowed_origins = extension_ids
.into_iter()
.map(|id| format!("chrome-extension://{id}/"))
.collect::<Vec<String>>();
let manifest = serde_json::json!({
"name": host_name,
"description": "gdown Native Messaging Host",
"path": runner_path,
"type": "stdio",
"allowed_origins": allowed_origins,
});
let manifest_text = serde_json::to_string_pretty(&manifest)
.map_err(|err| format!("manifest 직렬화 실패: {err}"))?;
fs::write(&manifest_path, manifest_text).map_err(|err| format!("manifest 쓰기 실패: {err}"))?;
Ok(manifest_path)
}
fn external_add_queue_path() -> Result<PathBuf, String> {
let home = std::env::var("HOME").map_err(|err| format!("HOME 경로 확인 실패: {err}"))?;
Ok(PathBuf::from(home).join(".gdown").join("external_add_queue.jsonl"))
@@ -102,6 +243,13 @@ pub fn run() {
}
})
.setup(|app| {
#[cfg(target_os = "macos")]
{
if let Err(err) = ensure_native_host_installed() {
eprintln!("[gdown] native host auto-install skipped: {err}");
}
}
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()

View File

@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "gdown",
"version": "0.1.1",
"version": "0.1.2",
"identifier": "com.tauri.dev",
"build": {
"frontendDist": "../dist",
@@ -15,9 +15,9 @@
"label": "main",
"title": "gdown",
"width": 1280,
"height": 860,
"height": 660,
"minWidth": 1080,
"minHeight": 720,
"minHeight": 600,
"resizable": true,
"fullscreen": false
}