2026-02-24 00:51:25 +09:00
|
|
|
mod engine;
|
2026-02-25 01:40:52 +09:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-02-25 11:12:23 +09:00
|
|
|
use std::env;
|
2026-02-25 01:40:52 +09:00
|
|
|
use std::fs;
|
|
|
|
|
use std::io::Write;
|
2026-02-25 11:12:23 +09:00
|
|
|
use std::path::Path;
|
2026-02-25 01:40:52 +09:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use tauri::Manager;
|
2026-02-24 00:51:25 +09:00
|
|
|
|
|
|
|
|
use engine::{
|
2026-02-25 01:40:52 +09:00
|
|
|
aria2_add_torrent, aria2_add_uri, aria2_change_global_option, aria2_get_task_detail,
|
|
|
|
|
aria2_list_tasks, aria2_pause_all, aria2_pause_task, aria2_remove_task,
|
|
|
|
|
aria2_remove_task_record, aria2_resume_all, aria2_resume_task, detect_aria2_binary,
|
|
|
|
|
engine_start, engine_status, engine_stop,
|
|
|
|
|
load_torrent_file,
|
|
|
|
|
open_path_in_file_manager, stop_engine_for_exit, EngineState,
|
2026-02-24 00:51:25 +09:00
|
|
|
};
|
|
|
|
|
|
2026-02-25 01:40:52 +09:00
|
|
|
#[tauri::command]
|
|
|
|
|
async fn focus_main_window(app: tauri::AppHandle) -> Result<(), String> {
|
|
|
|
|
let window = app
|
|
|
|
|
.get_webview_window("main")
|
|
|
|
|
.ok_or_else(|| "메인 창을 찾을 수 없습니다.".to_string())?;
|
|
|
|
|
|
|
|
|
|
window
|
|
|
|
|
.show()
|
|
|
|
|
.map_err(|error| format!("창 표시 실패: {error}"))?;
|
|
|
|
|
window
|
|
|
|
|
.unminimize()
|
|
|
|
|
.map_err(|error| format!("창 복원 실패: {error}"))?;
|
|
|
|
|
window
|
|
|
|
|
.set_focus()
|
|
|
|
|
.map_err(|error| format!("창 포커스 실패: {error}"))?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase")]
|
|
|
|
|
struct ExternalAddRequest {
|
|
|
|
|
url: String,
|
|
|
|
|
out: Option<String>,
|
|
|
|
|
dir: Option<String>,
|
|
|
|
|
referer: Option<String>,
|
|
|
|
|
user_agent: Option<String>,
|
|
|
|
|
authorization: Option<String>,
|
|
|
|
|
cookie: Option<String>,
|
|
|
|
|
proxy: Option<String>,
|
|
|
|
|
split: Option<u32>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 11:12:23 +09:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 01:40:52 +09:00
|
|
|
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"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
fn take_external_add_requests() -> Result<Vec<ExternalAddRequest>, String> {
|
|
|
|
|
let path = external_add_queue_path()?;
|
|
|
|
|
let parent = path
|
|
|
|
|
.parent()
|
|
|
|
|
.ok_or_else(|| "큐 디렉터리 경로를 계산할 수 없습니다.".to_string())?;
|
|
|
|
|
fs::create_dir_all(parent).map_err(|err| format!("큐 디렉터리 생성 실패: {err}"))?;
|
|
|
|
|
|
|
|
|
|
if !path.exists() {
|
|
|
|
|
return Ok(Vec::new());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let content = fs::read_to_string(&path).map_err(|err| format!("큐 읽기 실패: {err}"))?;
|
|
|
|
|
let mut requests: Vec<ExternalAddRequest> = Vec::new();
|
|
|
|
|
for line in content.lines() {
|
|
|
|
|
let trimmed = line.trim();
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if let Ok(item) = serde_json::from_str::<ExternalAddRequest>(trimmed) {
|
|
|
|
|
requests.push(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut file = fs::OpenOptions::new()
|
|
|
|
|
.write(true)
|
|
|
|
|
.truncate(true)
|
|
|
|
|
.open(&path)
|
|
|
|
|
.map_err(|err| format!("큐 초기화 실패: {err}"))?;
|
|
|
|
|
file
|
|
|
|
|
.write_all(b"")
|
|
|
|
|
.map_err(|err| format!("큐 파일 쓰기 실패: {err}"))?;
|
|
|
|
|
|
|
|
|
|
Ok(requests)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 00:51:25 +09:00
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
|
|
|
pub fn run() {
|
|
|
|
|
tauri::Builder::default()
|
|
|
|
|
.manage(EngineState::default())
|
2026-02-24 12:00:30 +09:00
|
|
|
.plugin(tauri_plugin_dialog::init())
|
2026-02-25 01:40:52 +09:00
|
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
|
|
|
.on_window_event(|window, event| {
|
|
|
|
|
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
|
|
|
|
if window.label() == "main" {
|
|
|
|
|
let state = window.state::<EngineState>();
|
|
|
|
|
stop_engine_for_exit(&state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-02-24 00:51:25 +09:00
|
|
|
.setup(|app| {
|
2026-02-25 11:12:23 +09:00
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
{
|
|
|
|
|
if let Err(err) = ensure_native_host_installed() {
|
|
|
|
|
eprintln!("[gdown] native host auto-install skipped: {err}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 00:51:25 +09:00
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
app.handle().plugin(
|
|
|
|
|
tauri_plugin_log::Builder::default()
|
|
|
|
|
.level(log::LevelFilter::Info)
|
|
|
|
|
.build(),
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.invoke_handler(tauri::generate_handler![
|
|
|
|
|
engine_start,
|
|
|
|
|
engine_stop,
|
|
|
|
|
engine_status,
|
2026-02-24 12:00:30 +09:00
|
|
|
detect_aria2_binary,
|
2026-02-24 00:51:25 +09:00
|
|
|
aria2_add_torrent,
|
|
|
|
|
aria2_add_uri,
|
2026-02-25 01:40:52 +09:00
|
|
|
aria2_change_global_option,
|
|
|
|
|
aria2_get_task_detail,
|
2026-02-24 00:51:25 +09:00
|
|
|
aria2_list_tasks,
|
2026-02-24 12:00:30 +09:00
|
|
|
aria2_pause_task,
|
|
|
|
|
aria2_resume_task,
|
|
|
|
|
aria2_remove_task,
|
|
|
|
|
aria2_remove_task_record,
|
|
|
|
|
aria2_pause_all,
|
|
|
|
|
aria2_resume_all,
|
|
|
|
|
load_torrent_file,
|
2026-02-25 01:40:52 +09:00
|
|
|
open_path_in_file_manager,
|
|
|
|
|
focus_main_window,
|
|
|
|
|
take_external_add_requests
|
2026-02-24 00:51:25 +09:00
|
|
|
])
|
|
|
|
|
.run(tauri::generate_context!())
|
|
|
|
|
.expect("error while running tauri application");
|
|
|
|
|
}
|