Skip to main content

unlab_gpu/pkg/
bitbucket.rs

1//
2// Copyright (c) 2026 Ɓukasz Szpakowski
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7//
8//! A module of [Bitbucket](https://bitbucket.org) source.
9use std::str;
10use crate::serde_json;
11use super::*;
12
13/// A service domain.
14pub const SERVICE_DOMAIN: &'static str = "bitbucket.org";
15/// A service API domain.
16pub const SERVICE_API_DOMAIN: &'static str = "api.bitbucket.org";
17
18#[derive(Clone, Debug, Deserialize)]
19struct Tag
20{
21    name: String,
22}
23
24#[derive(Clone, Debug, Deserialize)]
25struct Tags
26{
27    values: Vec<Tag>,
28}
29
30/// A structure of [Bitbucket](https://bitbucket.org) source.
31#[derive(Clone)]
32pub struct BitbucketSrc
33{
34    name: PkgName,
35    old_name: Option<PkgName>,
36    home_dir: PathBuf,
37    work_dir: PathBuf,
38    printer: Arc<dyn Print + Send + Sync>,
39    versions: Option<BTreeSet<Version>>,
40    current_version: Option<Version>,
41    dir: Option<PathBuf>,
42}
43
44impl BitbucketSrc
45{
46    /// Creates a [Bitbucket](https://bitbucket.org) source.
47    pub fn new(name: PkgName, old_name: Option<PkgName>, home_dir: PathBuf, work_dir: PathBuf, printer: Arc<dyn Print + Send + Sync>) -> Option<Self>
48    {
49        let original_name = old_name.as_ref().unwrap_or(&name);
50        if original_name.name().split('/').count() != 3 {
51            return None;
52        }
53        match original_name.name().split_once('/') {
54            Some((domain, _)) if domain == SERVICE_DOMAIN => {
55                Some(BitbucketSrc {
56                        name,
57                        old_name,
58                        home_dir,
59                        work_dir,
60                        printer,
61                        versions: None,
62                        current_version: None,
63                        dir: None,
64                })
65            },
66            _ => None,
67        }
68    }
69
70    /// Returns the package name.
71    pub fn name(&self) -> &PkgName
72    { &self.name }
73
74    /// Returns the old package name if the source has the old package name, otherwise `None`.
75    pub fn old_name(&self) -> Option<&PkgName>
76    { 
77        match &self.old_name {
78            Some(old_name) => Some(old_name),
79            None => None,
80        }
81    }
82
83    /// Returns the path to the Unlab-gpu home directory.
84    pub fn home_dir(&self) -> &Path
85    { self.home_dir.as_path() }
86
87    /// Returns the path to the work directory of current package.
88    pub fn work_dir(&self) -> &Path
89    { self.work_dir.as_path() }
90
91    /// Returns the printer.
92    pub fn printer(&self) -> &Arc<dyn Print + Send + Sync>
93    { &self.printer }
94
95    /// Returns the current package version.
96    pub fn current_version(&self) -> Option<&Version>
97    { 
98        match &self.current_version {
99            Some(current_version) => Some(current_version),
100            None => None,
101        }
102    }
103
104    fn update_versions(&self, is_update: bool) -> Result<BTreeSet<Version>>
105    {
106        let original_name = self.old_name.as_ref().unwrap_or(&self.name);
107        let repo_path = match original_name.name().split_once('/') {
108            Some((_, tmp_repo_path)) => tmp_repo_path,
109            None => return Err(Error::PkgName(self.name.clone(), String::from("no package repository path"))),
110        };
111        update_pkg_versions(&self.name, &self.old_name, self.home_dir.as_path(), is_update, &self.printer, || {
112                let mut easy = curl::easy::Easy::new();
113                easy.url(format!("https://{}/2.0/repositories/{}/refs/tags", SERVICE_API_DOMAIN, str_to_url_name(repo_path, true)).as_str())?;
114                let mut http_headers = List::new();
115                http_headers.append(USER_AGENT_HTTP_HEADER)?;
116                easy.http_headers(http_headers)?;
117                easy.follow_location(true)?;
118                Ok(easy)
119        }, |data| {
120                let s = match str::from_utf8(data) {
121                    Ok(tmp_s) => tmp_s,
122                    Err(_) => return Err(Error::PkgName(self.name.clone(), String::from("data contains invalid UTF-8 character"))),
123                };
124                let tags: Tags = match serde_json::from_str(s) {
125                    Ok(tmp_refs) => tmp_refs,
126                    Err(err) => return Err(Error::SerdeJson(err)),
127                };
128                let mut versions: BTreeSet<Version> = BTreeSet::new();
129                for tag in &tags.values {
130                    match tag_name_to_version(tag.name.as_str()) {
131                        Some(version) => {
132                            versions.insert(version);
133                        },
134                        None => (),
135                    }
136                }
137                Ok(versions)
138        })
139    }
140}
141
142impl Source for BitbucketSrc
143{
144    fn update(&mut self) -> Result<()>
145    {
146        self.versions = Some(self.update_versions(true)?);
147        Ok(())
148    }
149    
150    fn versions(&mut self) -> Result<&BTreeSet<Version>>
151    {
152        if self.versions.is_none() {
153            self.versions = Some(self.update_versions(false)?);
154        }
155        match &self.versions {
156            Some(versions) => Ok(versions),
157            None => return Err(Error::PkgName(self.name.clone(), String::from("no package versions"))),
158        }
159    }
160    
161    fn set_current_version(&mut self, version: Version)
162    { self.current_version = Some(version); }
163    
164    fn dir(&mut self) -> Result<&Path>
165    {
166        if self.dir.is_none() {
167            match &self.current_version {
168                Some(current_version) => {
169                    self.dir = Some(extract_pkg_file(&self.name, current_version, &self.work_dir, &self.printer, || {
170                            let original_name = self.old_name.as_ref().unwrap_or(&self.name);
171                            let tag_name = version_to_tag_name(current_version);
172                            let url = format!("https://{}/get/{}.zip", str_to_url_name(original_name.name(), true), str_to_url_name(tag_name.as_str(), false));
173                            download_pkg_file(&self.name, &self.old_name, current_version, url.as_str(), &self.home_dir, &self.printer)
174                    })?)
175                },
176                None => return Err(Error::PkgName(self.name.clone(), String::from("no current package version"))),
177            }
178        }
179        match &self.dir {
180            Some(versions) => Ok(versions),
181            None => return Err(Error::PkgName(self.name.clone(), String::from("no package directory"))),
182        }
183    }
184}
185
186/// A structure of factory of [Bitbucket](https://bitbucket.org) source.
187#[derive(Copy, Clone, Debug)]
188pub struct BitbucketSrcFactory;
189
190impl BitbucketSrcFactory
191{
192    /// Creates a factory of [Bitbucket](https://bitbucket.org) source.
193    pub fn new() -> Self
194    { BitbucketSrcFactory }
195}
196
197impl SourceCreate for BitbucketSrcFactory
198{
199    fn create(&self, name: PkgName, old_name: Option<PkgName>, home_dir: PathBuf, work_dir: PathBuf, printer: Arc<dyn Print + Send + Sync>) -> Option<Box<dyn Source + Send + Sync>>
200    { 
201        match BitbucketSrc::new(name, old_name, home_dir, work_dir, printer) {
202            Some(src) => Some(Box::new(src)),
203            None => None,
204        }
205    }
206}