1use std::{io::Read, sync::LazyLock};
4
5use anyhow::{Result, anyhow, bail};
6use bytes::Buf;
7use futures::executor::block_on;
8use libabbs::apml::{
9 ApmlContext,
10 value::{array::StringArray, union::Union},
11};
12use log::{debug, info, warn};
13use opendal::{
14 Operator,
15 layers::RetryLayer,
16 services::{Github, Memory},
17};
18use regex::Regex;
19use reqwest::ClientBuilder;
20use tempfile::tempfile;
21
22pub mod pypi;
23
24static REGEX_GH_URL: LazyLock<Regex> = LazyLock::new(|| {
25 Regex::new(
26 r##"http(s|)://github\.com/(?<user>[a-zA-Z_-]+)/(?<repo>[a-zA-Z_-]+)"##,
27 )
28 .unwrap()
29});
30
31pub async fn open(ctx: ApmlContext) -> Result<Operator> {
33 let srcs = ctx.read("SRCS").into_string();
34 let version = ctx.read("VER").into_string();
35 let srcs = StringArray::from(srcs);
36
37 if srcs.len() == 1 {
38 let src = srcs[0].clone();
39 let un = if src.starts_with("https://") || src.starts_with("http://") {
40 Union::try_from(format!("tbl::{src}").as_str())?
41 } else {
42 Union::try_from(src.as_str())?
43 };
44
45 match un.tag.as_str() {
46 "tarball" | "tbl" => {
47 if let Some(url) = un.argument {
48 if let Some(fs) = find_alt_fs(&url).await? {
49 return Ok(fs);
50 }
51 return fetch_tarball(url).await;
52 }
53 }
54 "git" => {
55 if let Some(url) = un.argument
56 && let Some(fs) = find_alt_fs(&url).await? {
57 return Ok(fs);
58 }
59 }
60 "pypi" => {
61 if let Some(package) = un.argument {
62 return pypi::load(
63 &package,
64 un.properties.get("version").unwrap_or(&version),
65 )
66 .await;
67 }
68 }
69 _ => {
70 warn!("unsupported source type: {}", un.tag);
71 }
72 }
73 warn!("failed to recognize source provider: {}", &src);
74 } else {
75 warn!("multiple sources are not supported yet");
76 }
77 Ok(Operator::new(Memory::default())?.finish())
78}
79
80async fn find_alt_fs(url: &str) -> Result<Option<Operator>> {
86 if let Some(cap) = REGEX_GH_URL.captures(url) {
87 let owner = &cap["user"];
88 let repo = &cap["repo"];
89 debug!(
90 "recognized GitHub repository {owner}/{repo} from {url}"
91 );
92 Ok(Some(
93 Operator::new(Github::default().owner(owner).repo(repo))?
94 .layer(RetryLayer::new())
95 .finish(),
96 ))
97 } else {
98 Ok(None)
99 }
100}
101
102fn http_client() -> Result<reqwest::Client> {
103 Ok(ClientBuilder::new()
104 .user_agent(format!(
105 "libpfu/{} (https://github.com/AOSC-Dev/pfu)",
106 env!("CARGO_PKG_VERSION")
107 ))
108 .build()?)
109}
110
111async fn fetch_tarball(url: String) -> Result<Operator> {
113 info!("Downloading tarball: {url}");
114 let client = http_client()?;
115 let resp = client
116 .execute(client.get(&url).build()?)
117 .await?
118 .error_for_status()?;
119
120 let reader = resp.bytes().await?.reader();
121 let fs = block_on(async { load_compressed_tarball(&url, reader).await })?;
122 Ok(fs)
123}
124
125async fn load_compressed_tarball(
127 name: &str,
128 reader: impl Read,
129) -> Result<Operator> {
130 if name.ends_with(".tar") {
131 debug!("Recognized bare tarball");
132 load_tarball(reader).await
133 } else if name.ends_with(".tar.gz")
134 || name.ends_with(".tar.gzip")
135 || name.ends_with(".tgz")
136 {
137 debug!("Recognized tarball + gzip");
138 let reader = flate2::read::GzDecoder::new(reader);
139 load_tarball(reader).await
140 } else if name.ends_with(".tar.xz") {
141 debug!("Recognized tarball + XZ");
142 let reader = xz2::read::XzDecoder::new(reader);
143 load_tarball(reader).await
144 } else if name.ends_with(".tar.zst") || name.ends_with(".tar.zstd") {
145 debug!("Recognized tarball + zstd");
146 let reader = zstd::Decoder::new(reader)?;
147 load_tarball(reader).await
148 } else if name.ends_with(".tar.bz")
149 || name.ends_with(".tar.bz2")
150 || name.ends_with(".tar.bzip")
151 {
152 debug!("Recognized tarball + bz");
153 let reader = bzip2::read::BzDecoder::new(reader);
154 load_tarball(reader).await
155 } else {
156 bail!("unsupported archive type")
157 }
158}
159
160async fn load_tarball(mut reader: impl Read) -> Result<Operator> {
162 let fs = Operator::new(Memory::default())?.finish();
163
164 let mut temp = tempfile()?;
165 std::io::copy(&mut reader, &mut temp)?;
166
167 let mut tar = tar::Archive::new(temp);
168 for entry in tar.entries()? {
169 let mut entry = entry?;
170 if entry.header().entry_type() == tar::EntryType::Directory {
171 fs.create_dir(
172 entry
173 .path()?
174 .to_str()
175 .ok_or_else(|| anyhow!("invalid dir name in tarball"))?,
176 )
177 .await?;
178 } else {
179 let path = entry.path()?.to_path_buf();
180 if let Some(parent) = path.parent() {
181 fs.create_dir(
182 parent.to_str().ok_or_else(|| {
183 anyhow!("invalid dir name in tarball")
184 })?,
185 )
186 .await?;
187 }
188 let mut buf = Vec::with_capacity(entry.size() as usize);
189 entry.read_to_end(&mut buf)?;
190 fs.write(
191 path.to_str()
192 .ok_or_else(|| anyhow!("invalid filename in tarball"))?,
193 buf,
194 )
195 .await?;
196 }
197 }
198
199 Ok(fs)
200}