Files
gdown/src-tauri/src/lib.rs

289 lines
8.4 KiB
Rust

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;
use engine::{
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, yt_dlp_add_uri, EngineState,
};
#[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>,
extractor: Option<String>,
format: Option<String>,
}
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"))
}
#[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)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(EngineState::default())
.plugin(tauri_plugin_dialog::init())
.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);
}
}
})
.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()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
engine_start,
engine_stop,
engine_status,
detect_aria2_binary,
aria2_add_torrent,
aria2_add_uri,
yt_dlp_add_uri,
aria2_change_global_option,
aria2_get_task_detail,
aria2_list_tasks,
aria2_pause_task,
aria2_resume_task,
aria2_remove_task,
aria2_remove_task_record,
aria2_pause_all,
aria2_resume_all,
load_torrent_file,
open_path_in_file_manager,
focus_main_window,
take_external_add_requests
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}