Skip to main content

uxn_tal/fetch/
patchstorage.rs

1use super::{
2    downloader::{http_get, write_bytes},
3    provider::{FetchResult, Provider, RepoRef},
4};
5use std::path::{Path, PathBuf};
6use url::Url;
7
8pub struct PatchStorage;
9
10impl PatchStorage {
11    fn extract_download_url(html: &str) -> Option<String> {
12        // Use scraper to find the download <a> tag with .orca href
13        use scraper::{Html, Selector};
14        let document = Html::parse_document(html);
15        let selector = Selector::parse("a.ps-patch-download").ok()?;
16        for element in document.select(&selector) {
17            if let Some(href) = element.value().attr("href") {
18                if href.ends_with(".orca") {
19                    if href.starts_with("http") {
20                        return Some(href.to_string());
21                    } else {
22                        return Some(format!("https://patchstorage.com{}", href));
23                    }
24                }
25            }
26        }
27        None
28    }
29
30    fn fetch_file(url: &str, out_root: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
31        let html = http_get(url)?;
32        let html_str = String::from_utf8_lossy(&html);
33        let download_url = Self::extract_download_url(&html_str)
34            .ok_or_else(|| "PatchStorage: Download link not found".to_string())?;
35        let file_bytes = http_get(&download_url)?;
36        let filename = download_url
37            .split('/')
38            .next_back()
39            .ok_or("PatchStorage: Invalid download URL")?;
40        let local = out_root.join(filename);
41        write_bytes(&local, &file_bytes)?;
42        Ok(local)
43    }
44}
45
46impl Provider for PatchStorage {
47    fn parse_url(&self, url: &str) -> Option<RepoRef> {
48        let u = Url::parse(url).ok()?;
49        if u.domain()? != "patchstorage.com" {
50            return None;
51        }
52        Some(RepoRef {
53            host: "patchstorage.com".into(),
54            owner: "".into(),
55            repo: "".into(),
56            branch: "".into(),
57            path: Some(u.path().trim_start_matches('/').to_string()),
58        })
59    }
60
61    fn fetch_tal_tree(
62        &self,
63        r: &RepoRef,
64        out_root: &Path,
65    ) -> Result<FetchResult, Box<dyn std::error::Error>> {
66        let url = format!(
67            "https://patchstorage.com/{}",
68            r.path.as_deref().unwrap_or("")
69        );
70        let filename = url.split('/').next_back().unwrap_or("");
71        let mut entry_file: Option<PathBuf> = None;
72        if !filename.is_empty() {
73            let local = out_root.join(filename);
74            if local.is_file() {
75                eprintln!("[PatchStorage] Using cached file: {}", local.display());
76                entry_file = Some(local);
77            }
78        }
79        // If filename is empty or not a file, look for any .orca file in the cache dir
80        if entry_file.is_none() {
81            if let Ok(entries) = std::fs::read_dir(out_root) {
82                for entry in entries.flatten() {
83                    let path = entry.path();
84                    if path.extension().map(|e| e == "orca").unwrap_or(false) && path.is_file() {
85                        eprintln!("[PatchStorage] Using cached file: {}", path.display());
86                        entry_file = Some(path);
87                        break;
88                    }
89                }
90            }
91        }
92        if let Some(local) = entry_file {
93            return Ok(FetchResult {
94                entry_local: local.clone(),
95                all_files: vec![local],
96            });
97        }
98        // Otherwise, fetch and cache
99        let local = Self::fetch_file(&url, out_root)?;
100        Ok(FetchResult {
101            entry_local: local.clone(),
102            all_files: vec![local],
103        })
104    }
105}