Skip to main content

open_build_service_api/
lib.rs

1use bytes::Bytes;
2use futures::future::BoxFuture;
3use futures::prelude::*;
4use futures::ready;
5use futures::stream::BoxStream;
6use md5::{Digest, Md5};
7use quick_xml::SeError;
8use quick_xml::name::QName;
9use quick_xml::{de::DeError, events::Event};
10use reqwest::{Body, Method, RequestBuilder, Response, header::CONTENT_TYPE};
11use serde::de::IntoDeserializer;
12use serde::{Deserialize, Serialize, de::DeserializeOwned};
13use std::collections::HashMap;
14use std::pin::Pin;
15use std::task::{Context, Poll};
16use strum_macros::Display;
17use thiserror::Error;
18use url::Url;
19
20#[derive(Debug, Error)]
21pub enum Error {
22    #[error("Request failed: {0}")]
23    RequestError(#[from] reqwest::Error),
24    #[error("Request deserialization failed: {0}")]
25    DeError(#[from] DeError),
26    #[error("Request serialization failed: {0}")]
27    SeError(#[from] SeError),
28    #[error("{0}")]
29    ApiError(ApiError),
30    #[error("Unexpected result")]
31    UnexpectedResult,
32    #[error("Invalid client url")]
33    InvalidUrl,
34}
35
36#[derive(Clone, Deserialize, Debug)]
37pub struct ApiErrorSummary {
38    #[serde(rename = "$value")]
39    pub summary: String,
40}
41
42#[derive(Clone, Deserialize, Debug)]
43pub struct ApiError {
44    #[serde(rename = "@code")]
45    pub code: String,
46    pub summary: ApiErrorSummary,
47}
48
49impl std::fmt::Display for ApiError {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
51        write!(f, "{}: {}", self.code, self.summary.summary)
52    }
53}
54
55type Result<T> = std::result::Result<T, Error>;
56
57#[derive(Clone, Copy, Default, Deserialize, Debug, Eq, PartialEq, Display)]
58#[serde(rename_all = "lowercase")]
59#[strum(serialize_all = "lowercase")]
60pub enum RebuildMode {
61    #[default]
62    Transitive,
63    Direct,
64    Local,
65}
66
67#[derive(Clone, Copy, Deserialize, Default, Debug, Eq, PartialEq, Display)]
68#[serde(rename_all = "lowercase")]
69#[strum(serialize_all = "lowercase")]
70pub enum BlockMode {
71    #[default]
72    All,
73    Local,
74    Never,
75}
76
77#[derive(Deserialize, Debug)]
78pub struct RepositoryMeta {
79    #[serde(rename = "@name")]
80    pub name: String,
81    #[serde(default, rename = "@rebuild")]
82    pub rebuild: RebuildMode,
83    #[serde(default, rename = "@block")]
84    pub block: BlockMode,
85
86    #[serde(default, rename = "arch")]
87    pub arches: Vec<String>,
88}
89
90#[derive(Deserialize, Debug)]
91pub struct ProjectMeta {
92    #[serde(rename = "@name")]
93    pub name: String,
94    #[serde(default, rename = "repository")]
95    pub repositories: Vec<RepositoryMeta>,
96}
97
98#[derive(Copy, Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
99#[serde(rename_all = "lowercase")]
100pub enum RepositoryCode {
101    Unknown,
102    Broken,
103    Scheduling,
104    Blocked,
105    Building,
106    Finished,
107    Publishing,
108    Published,
109    Unpublished,
110}
111
112impl std::fmt::Display for RepositoryCode {
113    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
114        self.serialize(fmt)
115    }
116}
117
118#[derive(Copy, Clone, Deserialize, Serialize, Debug, Eq, PartialEq)]
119#[serde(rename_all = "lowercase")]
120pub enum PackageCode {
121    Unresolvable,
122    Succeeded,
123    Dispatching,
124    Failed,
125    Broken,
126    Disabled,
127    Excluded,
128    Blocked,
129    Locked,
130    Unknown,
131    Scheduled,
132    Building,
133    Finished,
134}
135
136impl PackageCode {
137    pub fn is_final(&self) -> bool {
138        matches!(
139            self,
140            Self::Broken | Self::Disabled | Self::Excluded | Self::Failed | Self::Succeeded
141        )
142    }
143}
144
145impl std::fmt::Display for PackageCode {
146    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
147        self.serialize(fmt)
148    }
149}
150
151#[derive(Deserialize, Debug)]
152pub struct JobStatus {
153    pub code: Option<RepositoryCode>,
154    pub details: Option<String>,
155    pub workerid: Option<String>,
156    pub starttime: Option<u64>,
157    pub endtime: Option<u64>,
158    pub lastduration: Option<u64>,
159    pub hostarch: Option<String>,
160    pub arch: Option<String>,
161    pub jobid: Option<String>,
162    pub job: Option<String>,
163    pub attempt: Option<u32>,
164}
165
166#[derive(Deserialize, Debug)]
167pub struct BuildStatus {
168    #[serde(rename = "@package")]
169    pub package: String,
170    #[serde(rename = "@code")]
171    pub code: PackageCode,
172    #[serde(default, rename = "@dirty")]
173    pub dirty: bool,
174    pub details: Option<String>,
175}
176
177#[derive(Deserialize, Debug)]
178pub struct BuildHistoryEntry {
179    #[serde(rename = "@rev")]
180    pub rev: String,
181    #[serde(rename = "@srcmd5")]
182    pub srcmd5: String,
183    #[serde(rename = "@versrel")]
184    pub versrel: String,
185    #[serde(rename = "@bcnt")]
186    pub bcnt: String,
187    #[serde(rename = "@time")]
188    pub time: String,
189    #[serde(rename = "@duration")]
190    pub duration: String,
191}
192
193#[derive(Deserialize, Debug)]
194pub struct BuildHistory {
195    #[serde(default, rename = "entry")]
196    pub entries: Vec<BuildHistoryEntry>,
197}
198
199#[derive(Deserialize, Debug)]
200pub struct LinkInfo {
201    #[serde(rename = "@project")]
202    pub project: String,
203    #[serde(rename = "@package")]
204    pub package: String,
205    #[serde(default, rename = "@srcmd5")]
206    pub srcmd5: Option<String>,
207    #[serde(default, rename = "@xsrcmd5")]
208    pub xsrcmd5: Option<String>,
209    #[serde(default, rename = "@lsrcmd5")]
210    pub lsrcmd5: Option<String>,
211    #[serde(default, rename = "@missingok")]
212    pub missingok: bool,
213    #[serde(default, rename = "@error")]
214    pub error: Option<String>,
215}
216
217#[derive(Deserialize, Debug)]
218pub struct SourceDirectoryEntry {
219    #[serde(rename = "@name")]
220    pub name: String,
221    #[serde(rename = "@size")]
222    pub size: u64,
223    #[serde(rename = "@md5")]
224    pub md5: String,
225    #[serde(rename = "@mtime")]
226    pub mtime: u64,
227    #[serde(rename = "@originproject")]
228    pub originproject: Option<String>,
229    //available ?
230    //recommended ?
231    #[serde(rename = "@hash")]
232    pub hash: Option<String>,
233}
234
235fn empty_string_is_none<'de, D>(de: D) -> std::result::Result<Option<String>, D::Error>
236where
237    D: serde::Deserializer<'de>,
238{
239    let s = Option::deserialize(de)?;
240    match s.as_deref() {
241        Some("") => Ok(None),
242        _ => Ok(s),
243    }
244}
245
246#[derive(Deserialize, Debug)]
247pub struct SourceDirectory {
248    #[serde(rename = "@name")]
249    pub name: String,
250    #[serde(rename = "@rev")]
251    pub rev: Option<String>,
252    #[serde(default, rename = "@vrev", deserialize_with = "empty_string_is_none")]
253    pub vrev: Option<String>,
254    #[serde(rename = "@srcmd5")]
255    pub srcmd5: String,
256    #[serde(default, rename = "entry")]
257    pub entries: Vec<SourceDirectoryEntry>,
258    #[serde(default, rename = "linkinfo")]
259    pub linkinfo: Vec<LinkInfo>,
260}
261
262#[derive(Clone, Deserialize, Debug)]
263pub struct Revision {
264    #[serde(rename = "@rev")]
265    pub rev: String,
266    #[serde(rename = "@vrev")]
267    pub vrev: String,
268    pub srcmd5: String,
269    pub version: String,
270    pub time: u64,
271    pub user: String,
272    pub comment: Option<String>,
273}
274
275#[derive(Deserialize, Debug)]
276pub struct RevisionList {
277    #[serde(default, rename = "revision")]
278    pub revisions: Vec<Revision>,
279}
280
281#[derive(Clone, Deserialize, Serialize, Debug)]
282pub struct CommitEntry {
283    #[serde(rename = "@name")]
284    pub name: String,
285    #[serde(rename = "@md5")]
286    pub md5: String,
287}
288
289impl CommitEntry {
290    pub fn from_contents<T: AsRef<[u8]>>(name: String, contents: T) -> Self {
291        let md5 = base16ct::lower::encode_string(&Md5::digest(&contents));
292        Self { name, md5 }
293    }
294}
295
296#[derive(Deserialize, Debug)]
297#[serde(tag = "error", rename = "missing")]
298pub struct MissingEntries {
299    #[serde(rename = "entry")]
300    pub entries: Vec<CommitEntry>,
301}
302
303#[derive(Debug)]
304pub enum CommitResult {
305    Success(SourceDirectory),
306    MissingEntries(MissingEntries),
307}
308
309#[derive(Clone, Deserialize, Serialize, Debug)]
310pub struct CommitFileEntry {
311    pub name: String,
312    pub md5: String,
313}
314
315impl CommitFileEntry {
316    pub fn from_contents<T: AsRef<[u8]>>(name: String, contents: T) -> Self {
317        let md5 = base16ct::lower::encode_string(&Md5::digest(&contents));
318        Self { name, md5 }
319    }
320}
321
322#[derive(Clone, Debug, Default, Deserialize, Serialize)]
323#[serde(rename = "directory")]
324pub struct CommitFileList {
325    #[serde(rename = "entry")]
326    entries: Vec<CommitFileEntry>,
327}
328
329impl CommitFileList {
330    pub fn new() -> Self {
331        CommitFileList::default()
332    }
333
334    pub fn add_entry(&mut self, entry: CommitFileEntry) {
335        self.entries.push(entry);
336    }
337
338    pub fn add_file_md5(&mut self, name: String, md5: String) {
339        self.add_entry(CommitFileEntry { name, md5 });
340    }
341
342    pub fn add_file_from_contents(&mut self, name: String, contents: &[u8]) {
343        self.add_entry(CommitFileEntry::from_contents(name, contents));
344    }
345
346    pub fn entry(mut self, entry: CommitFileEntry) -> Self {
347        self.add_entry(entry);
348        self
349    }
350
351    pub fn file_md5(mut self, name: String, md5: String) -> Self {
352        self.add_file_md5(name, md5);
353        self
354    }
355
356    pub fn file_from_contents(mut self, name: String, contents: &[u8]) -> Self {
357        self.add_file_from_contents(name, contents);
358        self
359    }
360}
361
362#[derive(Clone, Debug, Default)]
363pub struct CommitOptions {
364    pub comment: Option<String>,
365}
366
367#[derive(Clone, Debug, Default)]
368pub struct BranchOptions {
369    pub target_project: Option<String>,
370    pub target_package: Option<String>,
371    pub comment: Option<String>,
372    pub force: bool,
373    pub missingok: bool,
374
375    pub add_repositories_rebuild: Option<RebuildMode>,
376    pub add_repositories_block: Option<BlockMode>,
377}
378
379#[derive(Clone, Debug)]
380pub struct BranchStatus {
381    pub source_project: String,
382    pub source_package: String,
383    pub target_project: String,
384    pub target_package: String,
385}
386
387impl<'de> Deserialize<'de> for BranchStatus {
388    fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
389    where
390        D: serde::Deserializer<'de>,
391    {
392        #[derive(Deserialize)]
393        struct BranchStatusDataItem {
394            #[serde(rename = "@name")]
395            name: String,
396            #[serde(rename = "$value")]
397            value: String,
398        }
399
400        #[derive(Deserialize)]
401        struct BranchStatusData {
402            data: Vec<BranchStatusDataItem>,
403        }
404
405        #[derive(Deserialize)]
406        struct BranchStatusExpanded {
407            sourceproject: String,
408            sourcepackage: String,
409            targetproject: String,
410            targetpackage: String,
411        }
412
413        let data: HashMap<String, String> = BranchStatusData::deserialize(deserializer)?
414            .data
415            .into_iter()
416            .map(|BranchStatusDataItem { name, value }| (name, value))
417            .collect();
418
419        let expanded = BranchStatusExpanded::deserialize(data.into_deserializer())?;
420        Ok(BranchStatus {
421            source_project: expanded.sourceproject,
422            source_package: expanded.sourcepackage,
423            target_project: expanded.targetproject,
424            target_package: expanded.targetpackage,
425        })
426    }
427}
428
429#[derive(Deserialize, Debug)]
430pub struct PackageBuildMetaDisable {
431    #[serde(default, rename = "@repository")]
432    pub repository: Option<String>,
433    #[serde(default, rename = "@arch")]
434    pub arch: Option<String>,
435}
436
437#[derive(Deserialize, Debug, Default)]
438pub struct PackageBuildMeta {
439    #[serde(rename = "disable")]
440    pub disabled: Vec<PackageBuildMetaDisable>,
441}
442
443#[derive(Deserialize, Debug)]
444pub struct PackageMeta {
445    #[serde(rename = "@name")]
446    pub name: String,
447    #[serde(rename = "@project")]
448    pub project: String,
449    #[serde(default)]
450    pub build: PackageBuildMeta,
451}
452
453#[derive(Deserialize, Debug)]
454pub struct ResultListResult {
455    #[serde(rename = "@project")]
456    pub project: String,
457    #[serde(rename = "@repository")]
458    pub repository: String,
459    #[serde(rename = "@arch")]
460    pub arch: String,
461    #[serde(rename = "@code")]
462    pub code: RepositoryCode,
463    #[serde(default, rename = "@dirty")]
464    pub dirty: bool,
465    #[serde(default, rename = "status")]
466    pub statuses: Vec<BuildStatus>,
467}
468
469impl ResultListResult {
470    pub fn get_status(&self, package: &str) -> Option<&BuildStatus> {
471        self.statuses.iter().find(|s| s.package == package)
472    }
473}
474
475#[derive(Deserialize, Debug)]
476pub struct ResultList {
477    #[serde(rename = "@state")]
478    pub state: String,
479    #[serde(rename = "result")]
480    pub results: Vec<ResultListResult>,
481}
482
483#[derive(Clone, Deserialize, Debug)]
484pub struct Binary {
485    #[serde(rename = "@filename")]
486    pub filename: String,
487    #[serde(rename = "@size")]
488    pub size: u64,
489    #[serde(rename = "@mtime")]
490    pub mtime: u64,
491}
492
493#[derive(Clone, Deserialize, Debug)]
494pub struct BinaryList {
495    #[serde(default, rename = "binary")]
496    pub binaries: Vec<Binary>,
497}
498
499#[derive(Deserialize, Debug)]
500pub struct DirectoryEntry {
501    #[serde(rename = "@name")]
502    pub name: String,
503}
504
505#[derive(Deserialize, Debug)]
506pub struct Directory {
507    #[serde(default, rename = "entry")]
508    pub entries: Vec<DirectoryEntry>,
509}
510
511#[derive(Clone, Debug)]
512pub struct RebuildFilters {
513    packages: Vec<String>,
514}
515
516impl RebuildFilters {
517    pub fn empty() -> Self {
518        RebuildFilters {
519            packages: Vec::new(),
520        }
521    }
522
523    pub fn only_package(package: String) -> Self {
524        RebuildFilters::empty().package(package)
525    }
526
527    pub fn add_package(&mut self, package: String) {
528        self.packages.push(package);
529    }
530
531    pub fn package(mut self, package: String) -> Self {
532        self.add_package(package);
533        self
534    }
535}
536
537#[derive(Clone, Debug, Default)]
538pub struct JobHistoryFilters {
539    packages: Vec<String>,
540    codes: Vec<PackageCode>,
541    limit: Option<usize>,
542}
543
544impl JobHistoryFilters {
545    pub fn empty() -> Self {
546        Self::default()
547    }
548
549    pub fn only_package(package: String) -> Self {
550        JobHistoryFilters::empty().package(package)
551    }
552
553    pub fn add_package(&mut self, package: String) {
554        self.packages.push(package);
555    }
556
557    pub fn add_code(&mut self, code: PackageCode) {
558        self.codes.push(code);
559    }
560
561    pub fn set_limit(&mut self, limit: Option<usize>) {
562        self.limit = limit;
563    }
564
565    pub fn package(mut self, package: String) -> Self {
566        self.add_package(package);
567        self
568    }
569
570    pub fn code(mut self, code: PackageCode) -> Self {
571        self.add_code(code);
572        self
573    }
574
575    pub fn limit(mut self, limit: Option<usize>) -> Self {
576        self.set_limit(limit);
577        self
578    }
579}
580
581#[derive(Deserialize, Debug)]
582pub struct JobHist {
583    #[serde(rename = "@package")]
584    pub package: String,
585    #[serde(rename = "@rev")]
586    pub rev: String,
587    #[serde(rename = "@srcmd5")]
588    pub srcmd5: String,
589    #[serde(rename = "@versrel")]
590    pub versrel: String,
591    #[serde(rename = "@bcnt")]
592    pub bcnt: String,
593    #[serde(rename = "@readytime")]
594    pub readytime: u64,
595    #[serde(rename = "@starttime")]
596    pub starttime: u64,
597    #[serde(rename = "@endtime")]
598    pub endtime: u64,
599    #[serde(rename = "@code")]
600    pub code: PackageCode,
601    #[serde(rename = "@uri")]
602    pub uri: String,
603    #[serde(rename = "@workerid")]
604    pub workerid: String,
605    #[serde(rename = "@hostarch")]
606    pub hostarch: String,
607    #[serde(rename = "@reason")]
608    pub reason: String,
609    #[serde(rename = "@verifymd5")]
610    pub verifymd5: String,
611}
612
613#[derive(Deserialize, Debug)]
614pub struct JobHistList {
615    #[serde(default)]
616    pub jobhist: Vec<JobHist>,
617}
618
619#[derive(Deserialize, Debug)]
620struct LogEntryEntry {
621    #[serde(rename = "@size")]
622    size: usize,
623    #[serde(rename = "@mtime")]
624    mtime: u64,
625}
626
627#[derive(Deserialize, Debug)]
628struct LogEntry {
629    #[serde(rename = "entry")]
630    pub entries: Vec<LogEntryEntry>,
631}
632
633enum PackageLogRequest {
634    Initial,
635    Request(BoxFuture<'static, Result<Response>>),
636    Stream((BoxStream<'static, reqwest::Result<Bytes>>, bool)),
637}
638
639#[derive(Default)]
640pub struct PackageLogStreamOptions {
641    pub offset: Option<usize>,
642    pub end: Option<usize>,
643}
644
645pub struct PackageLogStream<'a> {
646    client: &'a Client,
647    url: Url,
648    offset: usize,
649    options: PackageLogStreamOptions,
650    request: PackageLogRequest,
651}
652
653impl<'a> PackageLogStream<'a> {
654    fn new(client: &'a Client, options: PackageLogStreamOptions, url: Url) -> Self {
655        Self {
656            client,
657            url,
658            offset: options.offset.unwrap_or(0),
659            options,
660            request: PackageLogRequest::Initial,
661        }
662    }
663
664    fn request_log(&self, offset: usize) -> Result<Url> {
665        let mut url = self.url.clone();
666        url.query_pairs_mut()
667            .append_pair("nostream", "1")
668            .append_pair("start", &format!("{offset}"));
669        if let Some(end) = self.options.end {
670            url.query_pairs_mut().append_pair("end", &end.to_string());
671        }
672        Ok(url)
673    }
674}
675
676impl Stream for PackageLogStream<'_> {
677    type Item = Result<Bytes>;
678
679    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
680        let me = self.get_mut();
681
682        loop {
683            match me.request {
684                PackageLogRequest::Initial => {
685                    let u = match me.request_log(me.offset) {
686                        Ok(u) => u,
687                        Err(e) => return Poll::Ready(Some(Err(e))),
688                    };
689                    let r = me.client.authenticated_request(Method::GET, u);
690                    let r = Client::send_with_error(r).boxed();
691                    me.request = PackageLogRequest::Request(r);
692                }
693                PackageLogRequest::Request(ref mut r) => match ready!(r.as_mut().poll(cx)) {
694                    Ok(r) => {
695                        me.request = PackageLogRequest::Stream((r.bytes_stream().boxed(), false))
696                    }
697                    Err(e) => return Poll::Ready(Some(Err(e))),
698                },
699                PackageLogRequest::Stream((ref mut stream, ref mut gotdata)) => {
700                    match ready!(stream.as_mut().poll_next(cx)) {
701                        Some(Err(e)) => return Poll::Ready(Some(Err(e.into()))),
702                        Some(Ok(b)) => {
703                            me.offset += b.len();
704                            *gotdata = true;
705                            return Poll::Ready(Some(Ok(b)));
706                        }
707                        None => {
708                            let gotdata = *gotdata;
709                            me.request = PackageLogRequest::Initial;
710                            if !gotdata || matches!(me.options.end, Some(end) if me.offset >= end) {
711                                return Poll::Ready(None);
712                            }
713                        }
714                    }
715                }
716            }
717        }
718    }
719}
720
721pub struct PackageLog<'a> {
722    client: &'a Client,
723    project: String,
724    package: String,
725    repository: String,
726    arch: String,
727}
728
729impl<'a> PackageLog<'a> {
730    fn request(&self) -> Result<Url> {
731        let mut u = self.client.base.clone();
732        u.path_segments_mut()
733            .map_err(|_| Error::InvalidUrl)?
734            .push("build")
735            .push(&self.project)
736            .push(&self.repository)
737            .push(&self.arch)
738            .push(&self.package)
739            .push("_log");
740        Ok(u)
741    }
742
743    pub fn stream(&self, options: PackageLogStreamOptions) -> Result<PackageLogStream<'a>> {
744        let u = self.request()?;
745        Ok(PackageLogStream::new(self.client, options, u))
746    }
747
748    /// Returns size and mtime
749    pub async fn entry(&self) -> Result<(usize, u64)> {
750        let mut u = self.request()?;
751        u.query_pairs_mut().append_pair("view", "entry");
752
753        let e: LogEntry = self.client.request(u).await?;
754        if let Some(entry) = e.entries.first() {
755            Ok((entry.size, entry.mtime))
756        } else {
757            Err(Error::UnexpectedResult)
758        }
759    }
760}
761
762#[derive(Clone, Copy, Debug)]
763enum BuildCommand<'b> {
764    JobStatus,
765    History,
766    Status,
767    DownloadBinary(&'b str),
768}
769
770impl AsRef<str> for BuildCommand<'_> {
771    fn as_ref(&self) -> &str {
772        match self {
773            BuildCommand::JobStatus => "_jobstatus",
774            BuildCommand::History => "_history",
775            BuildCommand::Status => "_status",
776            BuildCommand::DownloadBinary(binary) => binary,
777        }
778    }
779}
780
781#[derive(Debug, Clone)]
782pub struct PackageBuilder<'a> {
783    pub client: &'a Client,
784    pub project: String,
785    pub package: String,
786}
787
788impl<'a> PackageBuilder<'a> {
789    fn full_request(
790        &self,
791        repository: &str,
792        arch: &str,
793        command: Option<BuildCommand<'_>>,
794    ) -> Result<Url> {
795        let mut u = self.client.base.clone();
796
797        {
798            let mut path = u.path_segments_mut().map_err(|_| Error::InvalidUrl)?;
799
800            path.push("build")
801                .push(&self.project)
802                .push(repository)
803                .push(arch)
804                .push(&self.package);
805
806            if let Some(command) = command {
807                path.push(command.as_ref());
808            }
809        }
810
811        Ok(u)
812    }
813
814    async fn upload_file<T: Into<Body>>(
815        &self,
816        file: &str,
817        rev: Option<&str>,
818        data: T,
819    ) -> Result<()> {
820        let mut u = self.client.base.clone();
821        u.path_segments_mut()
822            .map_err(|_| Error::InvalidUrl)?
823            .push("source")
824            .push(&self.project)
825            .push(&self.package)
826            .push(file);
827
828        if let Some(rev) = rev {
829            u.query_pairs_mut().append_pair("rev", rev);
830        }
831
832        Client::send_with_error(
833            self.client
834                .authenticated_request(Method::PUT, u)
835                .header(CONTENT_TYPE, "application/octet-stream")
836                .body(data),
837        )
838        .await?;
839
840        Ok(())
841    }
842
843    pub async fn jobstatus(&self, repository: &str, arch: &str) -> Result<JobStatus> {
844        let u = self.full_request(repository, arch, Some(BuildCommand::JobStatus))?;
845        self.client.request(u).await
846    }
847
848    pub async fn history(&self, repository: &str, arch: &str) -> Result<BuildHistory> {
849        let u = self.full_request(repository, arch, Some(BuildCommand::History))?;
850        self.client.request(u).await
851    }
852
853    pub async fn status(&self, repository: &str, arch: &str) -> Result<BuildStatus> {
854        let u = self.full_request(repository, arch, Some(BuildCommand::Status))?;
855        self.client.request(u).await
856    }
857
858    pub async fn binary_file(
859        &self,
860        repository: &str,
861        arch: &str,
862        file: &str,
863    ) -> Result<impl Stream<Item = Result<Bytes>> + use<>> {
864        let u = self.full_request(repository, arch, Some(BuildCommand::DownloadBinary(file)))?;
865        Ok(
866            Client::send_with_error(self.client.authenticated_request(Method::GET, u))
867                .await?
868                .bytes_stream()
869                .map_err(|e| e.into()),
870        )
871    }
872
873    pub async fn binaries(&self, repository: &str, arch: &str) -> Result<BinaryList> {
874        let u = self.full_request(repository, arch, None)?;
875        self.client.request(u).await
876    }
877
878    pub async fn rebuild(&self) -> Result<()> {
879        let mut u = self.client.base.clone();
880        u.path_segments_mut()
881            .map_err(|_| Error::InvalidUrl)?
882            .push("build")
883            .push(&self.project);
884
885        u.query_pairs_mut().append_pair("cmd", "rebuild");
886        u.query_pairs_mut().append_pair("package", &self.package);
887
888        Client::send_with_error(self.client.authenticated_request(Method::POST, u)).await?;
889
890        Ok(())
891    }
892
893    pub fn log(&self, repository: &str, arch: &str) -> PackageLog<'a> {
894        PackageLog {
895            client: self.client,
896            project: self.project.clone(),
897            package: self.package.clone(),
898            repository: repository.to_owned(),
899            arch: arch.to_owned(),
900        }
901    }
902
903    pub async fn create(&self) -> Result<()> {
904        let mut u = self.client.base.clone();
905        u.path_segments_mut()
906            .map_err(|_| Error::InvalidUrl)?
907            .push("source")
908            .push(&self.project)
909            .push(&self.package)
910            .push("_meta");
911
912        self.upload_file("_meta", None, "<package/>").await?;
913        Ok(())
914    }
915
916    pub async fn delete(&self) -> Result<()> {
917        let mut u = self.client.base.clone();
918        u.path_segments_mut()
919            .map_err(|_| Error::InvalidUrl)?
920            .push("source")
921            .push(&self.project)
922            .push(&self.package);
923
924        Client::send_with_error(self.client.authenticated_request(Method::DELETE, u)).await?;
925
926        Ok(())
927    }
928
929    pub async fn revisions(&self) -> Result<RevisionList> {
930        let mut u = self.client.base.clone();
931        u.path_segments_mut()
932            .map_err(|_| Error::InvalidUrl)?
933            .push("source")
934            .push(&self.project)
935            .push(&self.package)
936            .push("_history");
937        self.client.request(u).await
938    }
939
940    fn list_url(&self, rev: Option<&str>) -> Result<reqwest::Url> {
941        let mut u = self.client.base.clone();
942        u.path_segments_mut()
943            .map_err(|_| Error::InvalidUrl)?
944            .push("source")
945            .push(&self.project)
946            .push(&self.package);
947
948        if let Some(rev) = rev {
949            u.query_pairs_mut().append_pair("rev", rev);
950        }
951
952        Ok(u)
953    }
954
955    pub async fn list(&self, rev: Option<&str>) -> Result<SourceDirectory> {
956        let u = self.list_url(rev)?;
957        self.client.request(u).await
958    }
959
960    pub async fn list_meta(&self, rev: Option<&str>) -> Result<SourceDirectory> {
961        let mut u = self.list_url(rev)?;
962        u.query_pairs_mut().append_pair("meta", "1");
963        self.client.request(u).await
964    }
965
966    pub async fn meta(&self) -> Result<PackageMeta> {
967        let mut u = self.client.base.clone();
968        u.path_segments_mut()
969            .map_err(|_| Error::InvalidUrl)?
970            .push("source")
971            .push(&self.project)
972            .push(&self.package)
973            .push("_meta");
974        self.client.request(u).await
975    }
976
977    pub async fn source_file(&self, file: &str) -> Result<impl Stream<Item = Result<Bytes>>> {
978        let mut u = self.client.base.clone();
979        u.path_segments_mut()
980            .map_err(|_| Error::InvalidUrl)?
981            .push("source")
982            .push(&self.project)
983            .push(&self.package)
984            .push(file);
985        Ok(
986            Client::send_with_error(self.client.authenticated_request(Method::GET, u))
987                .await?
988                .bytes_stream()
989                .map_err(|e| e.into()),
990        )
991    }
992
993    pub async fn upload_for_commit<T: Into<Body>>(&self, file: &str, data: T) -> Result<()> {
994        let mut u = self.client.base.clone();
995        u.path_segments_mut()
996            .map_err(|_| Error::InvalidUrl)?
997            .push("source")
998            .push(&self.project)
999            .push(&self.package)
1000            .push(file);
1001        self.upload_file(file, Some("repository"), data).await?;
1002        Ok(())
1003    }
1004
1005    pub async fn commit(
1006        &self,
1007        filelist: &CommitFileList,
1008        options: &CommitOptions,
1009    ) -> Result<CommitResult> {
1010        let mut u = self.client.base.clone();
1011        u.path_segments_mut()
1012            .map_err(|_| Error::InvalidUrl)?
1013            .push("source")
1014            .push(&self.project)
1015            .push(&self.package);
1016        u.query_pairs_mut().append_pair("cmd", "commitfilelist");
1017
1018        if let Some(comment) = &options.comment {
1019            u.query_pairs_mut().append_pair("comment", comment);
1020        }
1021
1022        let mut body = String::new();
1023        quick_xml::se::to_writer(&mut body, filelist)?;
1024
1025        let response = Client::send_with_error(
1026            self.client
1027                .authenticated_request(Method::POST, u)
1028                .header(CONTENT_TYPE, "application/xml")
1029                .body(body),
1030        )
1031        .await?
1032        .text()
1033        .await?;
1034
1035        // We determine whether or not there were missing entries by the
1036        // presence of the "error" key, then use that to choose what enum value
1037        // to deserialize to. Ideally, we would be able to use untagged enum
1038        // magic: https://stackoverflow.com/a/61219284/2097780
1039        // Unfortunately, serde implementation details collide with quick-xml to
1040        // result in that not functioning here:
1041        // https://github.com/serde-rs/serde/issues/1183
1042        // https://github.com/tafia/quick-xml/issues/190
1043        // https://github.com/tafia/quick-xml/issues/203
1044        // Untagged enum deserialization logic depends on private serde API
1045        // functions, so it's not possible to implement it cleanly in a custom
1046        // "Deserialize".
1047
1048        let mut reader = quick_xml::Reader::from_str(&response);
1049        reader.config_mut().trim_text(true);
1050        if let Event::Start(e) = reader.read_event().map_err(DeError::from)? {
1051            let mut is_missing = false;
1052            for attr in e.attributes() {
1053                let attr = attr.map_err(DeError::from)?;
1054                if attr.key == QName(b"error") {
1055                    if attr.value.as_ref() != b"missing" {
1056                        return Err(DeError::Custom(
1057                            "only supported value for 'error' is 'missing'".to_owned(),
1058                        )
1059                        .into());
1060                    }
1061
1062                    is_missing = true;
1063                    break;
1064                }
1065            }
1066
1067            Ok(if is_missing {
1068                CommitResult::MissingEntries(quick_xml::de::from_str(&response)?)
1069            } else {
1070                CommitResult::Success(quick_xml::de::from_str(&response)?)
1071            })
1072        } else {
1073            Err(DeError::UnexpectedStart(response.into()).into())
1074        }
1075    }
1076
1077    pub async fn branch(&self, options: &BranchOptions) -> Result<BranchStatus> {
1078        let mut u = self.client.base.clone();
1079        u.path_segments_mut()
1080            .map_err(|_| Error::InvalidUrl)?
1081            .push("source")
1082            .push(&self.project)
1083            .push(&self.package);
1084        u.query_pairs_mut().append_pair("cmd", "branch");
1085
1086        if let Some(target_project) = &options.target_project {
1087            u.query_pairs_mut()
1088                .append_pair("target_project", target_project);
1089        }
1090
1091        if let Some(target_package) = &options.target_package {
1092            u.query_pairs_mut()
1093                .append_pair("target_package", target_package);
1094        }
1095
1096        if let Some(comment) = &options.comment {
1097            u.query_pairs_mut().append_pair("comment", comment);
1098        }
1099
1100        if let Some(rebuild) = &options.add_repositories_rebuild {
1101            u.query_pairs_mut()
1102                .append_pair("add_repositories_rebuild", &rebuild.to_string());
1103        }
1104
1105        if let Some(block) = &options.add_repositories_block {
1106            u.query_pairs_mut()
1107                .append_pair("add_repositories_block", &block.to_string());
1108        }
1109
1110        if options.force {
1111            u.query_pairs_mut().append_pair("force", "1");
1112        }
1113
1114        if options.missingok {
1115            u.query_pairs_mut().append_pair("missingok", "1");
1116        }
1117
1118        self.client.post_request(u).await
1119    }
1120
1121    pub async fn result(&self) -> Result<ResultList> {
1122        let mut u = self.client.base.clone();
1123        u.path_segments_mut()
1124            .map_err(|_| Error::InvalidUrl)?
1125            .push("build")
1126            .push(&self.project)
1127            .push("_result");
1128        u.query_pairs_mut().append_pair("package", &self.package);
1129        self.client.request(u).await
1130    }
1131}
1132
1133pub struct ProjectBuilder<'a> {
1134    client: &'a Client,
1135    project: String,
1136}
1137
1138impl<'a> ProjectBuilder<'a> {
1139    pub fn package(self, package: String) -> PackageBuilder<'a> {
1140        PackageBuilder {
1141            client: self.client,
1142            project: self.project,
1143            package,
1144        }
1145    }
1146
1147    pub async fn delete(&self) -> Result<()> {
1148        let mut u = self.client.base.clone();
1149        u.path_segments_mut()
1150            .map_err(|_| Error::InvalidUrl)?
1151            .push("source")
1152            .push(&self.project);
1153
1154        Client::send_with_error(self.client.authenticated_request(Method::DELETE, u)).await?;
1155
1156        Ok(())
1157    }
1158
1159    pub async fn list_packages(&self) -> Result<Directory> {
1160        let mut u = self.client.base.clone();
1161        u.path_segments_mut()
1162            .map_err(|_| Error::InvalidUrl)?
1163            .push("source")
1164            .push(&self.project);
1165        self.client.request(u).await
1166    }
1167
1168    pub async fn meta(&self) -> Result<ProjectMeta> {
1169        let mut u = self.client.base.clone();
1170        u.path_segments_mut()
1171            .map_err(|_| Error::InvalidUrl)?
1172            .push("source")
1173            .push(&self.project)
1174            .push("_meta");
1175        self.client.request(u).await
1176    }
1177
1178    pub async fn result(&self) -> Result<ResultList> {
1179        let mut u = self.client.base.clone();
1180        u.path_segments_mut()
1181            .map_err(|_| Error::InvalidUrl)?
1182            .push("build")
1183            .push(&self.project)
1184            .push("_result");
1185        self.client.request(u).await
1186    }
1187
1188    pub async fn repositories(&self) -> Result<Vec<String>> {
1189        let mut u = self.client.base.clone();
1190        u.path_segments_mut()
1191            .map_err(|_| Error::InvalidUrl)?
1192            .push("build")
1193            .push(&self.project);
1194        Ok(self
1195            .client
1196            .request::<Directory>(u)
1197            .await?
1198            .entries
1199            .into_iter()
1200            .map(|e| e.name)
1201            .collect())
1202    }
1203
1204    pub async fn arches(&self, repository: &str) -> Result<Vec<String>> {
1205        let mut u = self.client.base.clone();
1206        u.path_segments_mut()
1207            .map_err(|_| Error::InvalidUrl)?
1208            .push("build")
1209            .push(&self.project)
1210            .push(repository);
1211        Ok(self
1212            .client
1213            .request::<Directory>(u)
1214            .await?
1215            .entries
1216            .into_iter()
1217            .map(|e| e.name)
1218            .collect())
1219    }
1220
1221    pub async fn rebuild(&self, filters: &RebuildFilters) -> Result<()> {
1222        let mut u = self.client.base.clone();
1223        u.path_segments_mut()
1224            .map_err(|_| Error::InvalidUrl)?
1225            .push("build")
1226            .push(&self.project);
1227
1228        u.query_pairs_mut().append_pair("cmd", "rebuild");
1229        for package in &filters.packages {
1230            u.query_pairs_mut().append_pair("package", package);
1231        }
1232
1233        Client::send_with_error(self.client.authenticated_request(Method::POST, u)).await?;
1234
1235        Ok(())
1236    }
1237
1238    pub async fn jobhistory(
1239        &self,
1240        repository: &str,
1241        arch: &str,
1242        filters: &JobHistoryFilters,
1243    ) -> Result<JobHistList> {
1244        let mut u = self.client.base.clone();
1245        u.path_segments_mut()
1246            .map_err(|_| Error::InvalidUrl)?
1247            .push("build")
1248            .push(&self.project)
1249            .push(repository)
1250            .push(arch)
1251            .push("_jobhistory");
1252
1253        for package in &filters.packages {
1254            u.query_pairs_mut().append_pair("package", package);
1255        }
1256
1257        for code in &filters.codes {
1258            u.query_pairs_mut().append_pair("code", &code.to_string());
1259        }
1260
1261        if let Some(limit) = &filters.limit {
1262            u.query_pairs_mut().append_pair("limit", &limit.to_string());
1263        }
1264
1265        self.client.request(u).await
1266    }
1267}
1268
1269#[derive(Clone)]
1270pub struct Client {
1271    base: Url,
1272    user: String,
1273    pass: String,
1274    client: reqwest::Client,
1275}
1276
1277impl std::fmt::Debug for Client {
1278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1279        f.debug_struct("Client")
1280            .field("base", &format_args!("{:?}", self.base))
1281            .field("user", &self.user)
1282            .field("pass", &"[redacted]")
1283            .field("client", &format_args!("{:?}", self.client))
1284            .finish()
1285    }
1286}
1287
1288impl Client {
1289    pub fn new(url: Url, user: String, pass: String) -> Self {
1290        Client {
1291            base: url,
1292            user,
1293            pass,
1294            client: reqwest::ClientBuilder::new()
1295                .user_agent(concat!("open-build-service-rs/", env!("CARGO_PKG_VERSION")))
1296                .build()
1297                .unwrap(),
1298        }
1299    }
1300
1301    pub fn url(&self) -> &Url {
1302        &self.base
1303    }
1304
1305    pub fn project(&self, project: String) -> ProjectBuilder<'_> {
1306        ProjectBuilder {
1307            client: self,
1308            project,
1309        }
1310    }
1311
1312    fn authenticated_request(&self, method: Method, url: Url) -> RequestBuilder {
1313        self.client
1314            .request(method, url)
1315            .basic_auth(&self.user, Some(&self.pass))
1316    }
1317
1318    async fn send_with_error(request: RequestBuilder) -> Result<Response> {
1319        let response = request.send().await?;
1320
1321        match response.error_for_status_ref() {
1322            Ok(_) => Ok(response),
1323            Err(e) => {
1324                if let Some(status) = e.status() {
1325                    if status.is_client_error() {
1326                        let data = response.text().await?;
1327                        let error = quick_xml::de::from_str(&data)?;
1328                        Err(Error::ApiError(error))
1329                    } else {
1330                        Err(e.into())
1331                    }
1332                } else {
1333                    Err(e.into())
1334                }
1335            }
1336        }
1337    }
1338
1339    async fn request<T: DeserializeOwned + std::fmt::Debug>(&self, url: Url) -> Result<T> {
1340        let data = Self::send_with_error(self.authenticated_request(Method::GET, url))
1341            .await?
1342            .text()
1343            .await?;
1344        quick_xml::de::from_str(&data).map_err(|e| e.into())
1345    }
1346
1347    async fn post_request<T: DeserializeOwned + std::fmt::Debug>(&self, url: Url) -> Result<T> {
1348        let data = Self::send_with_error(self.authenticated_request(Method::POST, url))
1349            .await?
1350            .text()
1351            .await?;
1352        quick_xml::de::from_str(&data).map_err(|e| e.into())
1353    }
1354}