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                            if b.is_empty() {
704                                continue;
705                            }
706                            me.offset += b.len();
707                            *gotdata = true;
708                            return Poll::Ready(Some(Ok(b)));
709                        }
710                        None => {
711                            let gotdata = *gotdata;
712                            me.request = PackageLogRequest::Initial;
713                            if !gotdata || matches!(me.options.end, Some(end) if me.offset >= end) {
714                                return Poll::Ready(None);
715                            }
716                        }
717                    }
718                }
719            }
720        }
721    }
722}
723
724pub struct PackageLog<'a> {
725    client: &'a Client,
726    project: String,
727    package: String,
728    repository: String,
729    arch: String,
730}
731
732impl<'a> PackageLog<'a> {
733    fn request(&self) -> Result<Url> {
734        let mut u = self.client.base.clone();
735        u.path_segments_mut()
736            .map_err(|_| Error::InvalidUrl)?
737            .push("build")
738            .push(&self.project)
739            .push(&self.repository)
740            .push(&self.arch)
741            .push(&self.package)
742            .push("_log");
743        Ok(u)
744    }
745
746    pub fn stream(&self, options: PackageLogStreamOptions) -> Result<PackageLogStream<'a>> {
747        let u = self.request()?;
748        Ok(PackageLogStream::new(self.client, options, u))
749    }
750
751    /// Returns size and mtime
752    pub async fn entry(&self) -> Result<(usize, u64)> {
753        let mut u = self.request()?;
754        u.query_pairs_mut().append_pair("view", "entry");
755
756        let e: LogEntry = self.client.request(u).await?;
757        if let Some(entry) = e.entries.first() {
758            Ok((entry.size, entry.mtime))
759        } else {
760            Err(Error::UnexpectedResult)
761        }
762    }
763}
764
765#[derive(Clone, Copy, Debug)]
766enum BuildCommand<'b> {
767    JobStatus,
768    History,
769    Status,
770    DownloadBinary(&'b str),
771}
772
773impl AsRef<str> for BuildCommand<'_> {
774    fn as_ref(&self) -> &str {
775        match self {
776            BuildCommand::JobStatus => "_jobstatus",
777            BuildCommand::History => "_history",
778            BuildCommand::Status => "_status",
779            BuildCommand::DownloadBinary(binary) => binary,
780        }
781    }
782}
783
784#[derive(Debug, Clone)]
785pub struct PackageBuilder<'a> {
786    pub client: &'a Client,
787    pub project: String,
788    pub package: String,
789}
790
791impl<'a> PackageBuilder<'a> {
792    fn full_request(
793        &self,
794        repository: &str,
795        arch: &str,
796        command: Option<BuildCommand<'_>>,
797    ) -> Result<Url> {
798        let mut u = self.client.base.clone();
799
800        {
801            let mut path = u.path_segments_mut().map_err(|_| Error::InvalidUrl)?;
802
803            path.push("build")
804                .push(&self.project)
805                .push(repository)
806                .push(arch)
807                .push(&self.package);
808
809            if let Some(command) = command {
810                path.push(command.as_ref());
811            }
812        }
813
814        Ok(u)
815    }
816
817    async fn upload_file<T: Into<Body>>(
818        &self,
819        file: &str,
820        rev: Option<&str>,
821        data: T,
822    ) -> Result<()> {
823        let mut u = self.client.base.clone();
824        u.path_segments_mut()
825            .map_err(|_| Error::InvalidUrl)?
826            .push("source")
827            .push(&self.project)
828            .push(&self.package)
829            .push(file);
830
831        if let Some(rev) = rev {
832            u.query_pairs_mut().append_pair("rev", rev);
833        }
834
835        Client::send_with_error(
836            self.client
837                .authenticated_request(Method::PUT, u)
838                .header(CONTENT_TYPE, "application/octet-stream")
839                .body(data),
840        )
841        .await?;
842
843        Ok(())
844    }
845
846    pub async fn jobstatus(&self, repository: &str, arch: &str) -> Result<JobStatus> {
847        let u = self.full_request(repository, arch, Some(BuildCommand::JobStatus))?;
848        self.client.request(u).await
849    }
850
851    pub async fn history(&self, repository: &str, arch: &str) -> Result<BuildHistory> {
852        let u = self.full_request(repository, arch, Some(BuildCommand::History))?;
853        self.client.request(u).await
854    }
855
856    pub async fn status(&self, repository: &str, arch: &str) -> Result<BuildStatus> {
857        let u = self.full_request(repository, arch, Some(BuildCommand::Status))?;
858        self.client.request(u).await
859    }
860
861    pub async fn binary_file(
862        &self,
863        repository: &str,
864        arch: &str,
865        file: &str,
866    ) -> Result<impl Stream<Item = Result<Bytes>> + use<>> {
867        let u = self.full_request(repository, arch, Some(BuildCommand::DownloadBinary(file)))?;
868        Ok(
869            Client::send_with_error(self.client.authenticated_request(Method::GET, u))
870                .await?
871                .bytes_stream()
872                .map_err(|e| e.into()),
873        )
874    }
875
876    pub async fn binaries(&self, repository: &str, arch: &str) -> Result<BinaryList> {
877        let u = self.full_request(repository, arch, None)?;
878        self.client.request(u).await
879    }
880
881    pub async fn rebuild(&self) -> Result<()> {
882        let mut u = self.client.base.clone();
883        u.path_segments_mut()
884            .map_err(|_| Error::InvalidUrl)?
885            .push("build")
886            .push(&self.project);
887
888        u.query_pairs_mut().append_pair("cmd", "rebuild");
889        u.query_pairs_mut().append_pair("package", &self.package);
890
891        Client::send_with_error(self.client.authenticated_request(Method::POST, u)).await?;
892
893        Ok(())
894    }
895
896    pub fn log(&self, repository: &str, arch: &str) -> PackageLog<'a> {
897        PackageLog {
898            client: self.client,
899            project: self.project.clone(),
900            package: self.package.clone(),
901            repository: repository.to_owned(),
902            arch: arch.to_owned(),
903        }
904    }
905
906    pub async fn create(&self) -> Result<()> {
907        let mut u = self.client.base.clone();
908        u.path_segments_mut()
909            .map_err(|_| Error::InvalidUrl)?
910            .push("source")
911            .push(&self.project)
912            .push(&self.package)
913            .push("_meta");
914
915        self.upload_file("_meta", None, "<package/>").await?;
916        Ok(())
917    }
918
919    pub async fn delete(&self) -> Result<()> {
920        let mut u = self.client.base.clone();
921        u.path_segments_mut()
922            .map_err(|_| Error::InvalidUrl)?
923            .push("source")
924            .push(&self.project)
925            .push(&self.package);
926
927        Client::send_with_error(self.client.authenticated_request(Method::DELETE, u)).await?;
928
929        Ok(())
930    }
931
932    pub async fn revisions(&self) -> Result<RevisionList> {
933        let mut u = self.client.base.clone();
934        u.path_segments_mut()
935            .map_err(|_| Error::InvalidUrl)?
936            .push("source")
937            .push(&self.project)
938            .push(&self.package)
939            .push("_history");
940        self.client.request(u).await
941    }
942
943    fn list_url(&self, rev: Option<&str>) -> Result<reqwest::Url> {
944        let mut u = self.client.base.clone();
945        u.path_segments_mut()
946            .map_err(|_| Error::InvalidUrl)?
947            .push("source")
948            .push(&self.project)
949            .push(&self.package);
950
951        if let Some(rev) = rev {
952            u.query_pairs_mut().append_pair("rev", rev);
953        }
954
955        Ok(u)
956    }
957
958    pub async fn list(&self, rev: Option<&str>) -> Result<SourceDirectory> {
959        let u = self.list_url(rev)?;
960        self.client.request(u).await
961    }
962
963    pub async fn list_meta(&self, rev: Option<&str>) -> Result<SourceDirectory> {
964        let mut u = self.list_url(rev)?;
965        u.query_pairs_mut().append_pair("meta", "1");
966        self.client.request(u).await
967    }
968
969    pub async fn meta(&self) -> Result<PackageMeta> {
970        let mut u = self.client.base.clone();
971        u.path_segments_mut()
972            .map_err(|_| Error::InvalidUrl)?
973            .push("source")
974            .push(&self.project)
975            .push(&self.package)
976            .push("_meta");
977        self.client.request(u).await
978    }
979
980    pub async fn source_file(&self, file: &str) -> Result<impl Stream<Item = Result<Bytes>>> {
981        let mut u = self.client.base.clone();
982        u.path_segments_mut()
983            .map_err(|_| Error::InvalidUrl)?
984            .push("source")
985            .push(&self.project)
986            .push(&self.package)
987            .push(file);
988        Ok(
989            Client::send_with_error(self.client.authenticated_request(Method::GET, u))
990                .await?
991                .bytes_stream()
992                .map_err(|e| e.into()),
993        )
994    }
995
996    pub async fn upload_for_commit<T: Into<Body>>(&self, file: &str, data: T) -> Result<()> {
997        let mut u = self.client.base.clone();
998        u.path_segments_mut()
999            .map_err(|_| Error::InvalidUrl)?
1000            .push("source")
1001            .push(&self.project)
1002            .push(&self.package)
1003            .push(file);
1004        self.upload_file(file, Some("repository"), data).await?;
1005        Ok(())
1006    }
1007
1008    pub async fn commit(
1009        &self,
1010        filelist: &CommitFileList,
1011        options: &CommitOptions,
1012    ) -> Result<CommitResult> {
1013        let mut u = self.client.base.clone();
1014        u.path_segments_mut()
1015            .map_err(|_| Error::InvalidUrl)?
1016            .push("source")
1017            .push(&self.project)
1018            .push(&self.package);
1019        u.query_pairs_mut().append_pair("cmd", "commitfilelist");
1020
1021        if let Some(comment) = &options.comment {
1022            u.query_pairs_mut().append_pair("comment", comment);
1023        }
1024
1025        let mut body = String::new();
1026        quick_xml::se::to_writer(&mut body, filelist)?;
1027
1028        let response = Client::send_with_error(
1029            self.client
1030                .authenticated_request(Method::POST, u)
1031                .header(CONTENT_TYPE, "application/xml")
1032                .body(body),
1033        )
1034        .await?
1035        .text()
1036        .await?;
1037
1038        // We determine whether or not there were missing entries by the
1039        // presence of the "error" key, then use that to choose what enum value
1040        // to deserialize to. Ideally, we would be able to use untagged enum
1041        // magic: https://stackoverflow.com/a/61219284/2097780
1042        // Unfortunately, serde implementation details collide with quick-xml to
1043        // result in that not functioning here:
1044        // https://github.com/serde-rs/serde/issues/1183
1045        // https://github.com/tafia/quick-xml/issues/190
1046        // https://github.com/tafia/quick-xml/issues/203
1047        // Untagged enum deserialization logic depends on private serde API
1048        // functions, so it's not possible to implement it cleanly in a custom
1049        // "Deserialize".
1050
1051        let mut reader = quick_xml::Reader::from_str(&response);
1052        reader.config_mut().trim_text(true);
1053        if let Event::Start(e) = reader.read_event().map_err(DeError::from)? {
1054            let mut is_missing = false;
1055            for attr in e.attributes() {
1056                let attr = attr.map_err(DeError::from)?;
1057                if attr.key == QName(b"error") {
1058                    if attr.value.as_ref() != b"missing" {
1059                        return Err(DeError::Custom(
1060                            "only supported value for 'error' is 'missing'".to_owned(),
1061                        )
1062                        .into());
1063                    }
1064
1065                    is_missing = true;
1066                    break;
1067                }
1068            }
1069
1070            Ok(if is_missing {
1071                CommitResult::MissingEntries(quick_xml::de::from_str(&response)?)
1072            } else {
1073                CommitResult::Success(quick_xml::de::from_str(&response)?)
1074            })
1075        } else {
1076            Err(DeError::UnexpectedStart(response.into()).into())
1077        }
1078    }
1079
1080    pub async fn branch(&self, options: &BranchOptions) -> Result<BranchStatus> {
1081        let mut u = self.client.base.clone();
1082        u.path_segments_mut()
1083            .map_err(|_| Error::InvalidUrl)?
1084            .push("source")
1085            .push(&self.project)
1086            .push(&self.package);
1087        u.query_pairs_mut().append_pair("cmd", "branch");
1088
1089        if let Some(target_project) = &options.target_project {
1090            u.query_pairs_mut()
1091                .append_pair("target_project", target_project);
1092        }
1093
1094        if let Some(target_package) = &options.target_package {
1095            u.query_pairs_mut()
1096                .append_pair("target_package", target_package);
1097        }
1098
1099        if let Some(comment) = &options.comment {
1100            u.query_pairs_mut().append_pair("comment", comment);
1101        }
1102
1103        if let Some(rebuild) = &options.add_repositories_rebuild {
1104            u.query_pairs_mut()
1105                .append_pair("add_repositories_rebuild", &rebuild.to_string());
1106        }
1107
1108        if let Some(block) = &options.add_repositories_block {
1109            u.query_pairs_mut()
1110                .append_pair("add_repositories_block", &block.to_string());
1111        }
1112
1113        if options.force {
1114            u.query_pairs_mut().append_pair("force", "1");
1115        }
1116
1117        if options.missingok {
1118            u.query_pairs_mut().append_pair("missingok", "1");
1119        }
1120
1121        self.client.post_request(u).await
1122    }
1123
1124    pub async fn result(&self) -> Result<ResultList> {
1125        let mut u = self.client.base.clone();
1126        u.path_segments_mut()
1127            .map_err(|_| Error::InvalidUrl)?
1128            .push("build")
1129            .push(&self.project)
1130            .push("_result");
1131        u.query_pairs_mut().append_pair("package", &self.package);
1132        self.client.request(u).await
1133    }
1134}
1135
1136pub struct ProjectBuilder<'a> {
1137    client: &'a Client,
1138    project: String,
1139}
1140
1141impl<'a> ProjectBuilder<'a> {
1142    pub fn package(self, package: String) -> PackageBuilder<'a> {
1143        PackageBuilder {
1144            client: self.client,
1145            project: self.project,
1146            package,
1147        }
1148    }
1149
1150    pub async fn delete(&self) -> Result<()> {
1151        let mut u = self.client.base.clone();
1152        u.path_segments_mut()
1153            .map_err(|_| Error::InvalidUrl)?
1154            .push("source")
1155            .push(&self.project);
1156
1157        Client::send_with_error(self.client.authenticated_request(Method::DELETE, u)).await?;
1158
1159        Ok(())
1160    }
1161
1162    pub async fn list_packages(&self) -> Result<Directory> {
1163        let mut u = self.client.base.clone();
1164        u.path_segments_mut()
1165            .map_err(|_| Error::InvalidUrl)?
1166            .push("source")
1167            .push(&self.project);
1168        self.client.request(u).await
1169    }
1170
1171    pub async fn meta(&self) -> Result<ProjectMeta> {
1172        let mut u = self.client.base.clone();
1173        u.path_segments_mut()
1174            .map_err(|_| Error::InvalidUrl)?
1175            .push("source")
1176            .push(&self.project)
1177            .push("_meta");
1178        self.client.request(u).await
1179    }
1180
1181    pub async fn result(&self) -> Result<ResultList> {
1182        let mut u = self.client.base.clone();
1183        u.path_segments_mut()
1184            .map_err(|_| Error::InvalidUrl)?
1185            .push("build")
1186            .push(&self.project)
1187            .push("_result");
1188        self.client.request(u).await
1189    }
1190
1191    pub async fn repositories(&self) -> Result<Vec<String>> {
1192        let mut u = self.client.base.clone();
1193        u.path_segments_mut()
1194            .map_err(|_| Error::InvalidUrl)?
1195            .push("build")
1196            .push(&self.project);
1197        Ok(self
1198            .client
1199            .request::<Directory>(u)
1200            .await?
1201            .entries
1202            .into_iter()
1203            .map(|e| e.name)
1204            .collect())
1205    }
1206
1207    pub async fn arches(&self, repository: &str) -> Result<Vec<String>> {
1208        let mut u = self.client.base.clone();
1209        u.path_segments_mut()
1210            .map_err(|_| Error::InvalidUrl)?
1211            .push("build")
1212            .push(&self.project)
1213            .push(repository);
1214        Ok(self
1215            .client
1216            .request::<Directory>(u)
1217            .await?
1218            .entries
1219            .into_iter()
1220            .map(|e| e.name)
1221            .collect())
1222    }
1223
1224    pub async fn rebuild(&self, filters: &RebuildFilters) -> Result<()> {
1225        let mut u = self.client.base.clone();
1226        u.path_segments_mut()
1227            .map_err(|_| Error::InvalidUrl)?
1228            .push("build")
1229            .push(&self.project);
1230
1231        u.query_pairs_mut().append_pair("cmd", "rebuild");
1232        for package in &filters.packages {
1233            u.query_pairs_mut().append_pair("package", package);
1234        }
1235
1236        Client::send_with_error(self.client.authenticated_request(Method::POST, u)).await?;
1237
1238        Ok(())
1239    }
1240
1241    pub async fn jobhistory(
1242        &self,
1243        repository: &str,
1244        arch: &str,
1245        filters: &JobHistoryFilters,
1246    ) -> Result<JobHistList> {
1247        let mut u = self.client.base.clone();
1248        u.path_segments_mut()
1249            .map_err(|_| Error::InvalidUrl)?
1250            .push("build")
1251            .push(&self.project)
1252            .push(repository)
1253            .push(arch)
1254            .push("_jobhistory");
1255
1256        for package in &filters.packages {
1257            u.query_pairs_mut().append_pair("package", package);
1258        }
1259
1260        for code in &filters.codes {
1261            u.query_pairs_mut().append_pair("code", &code.to_string());
1262        }
1263
1264        if let Some(limit) = &filters.limit {
1265            u.query_pairs_mut().append_pair("limit", &limit.to_string());
1266        }
1267
1268        self.client.request(u).await
1269    }
1270}
1271
1272#[derive(Clone)]
1273pub struct Client {
1274    base: Url,
1275    user: String,
1276    pass: String,
1277    client: reqwest::Client,
1278}
1279
1280impl std::fmt::Debug for Client {
1281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1282        f.debug_struct("Client")
1283            .field("base", &format_args!("{:?}", self.base))
1284            .field("user", &self.user)
1285            .field("pass", &"[redacted]")
1286            .field("client", &format_args!("{:?}", self.client))
1287            .finish()
1288    }
1289}
1290
1291impl Client {
1292    pub fn new(url: Url, user: String, pass: String) -> Self {
1293        Client {
1294            base: url,
1295            user,
1296            pass,
1297            client: reqwest::ClientBuilder::new()
1298                .user_agent(concat!("open-build-service-rs/", env!("CARGO_PKG_VERSION")))
1299                .build()
1300                .unwrap(),
1301        }
1302    }
1303
1304    pub fn url(&self) -> &Url {
1305        &self.base
1306    }
1307
1308    pub fn project(&self, project: String) -> ProjectBuilder<'_> {
1309        ProjectBuilder {
1310            client: self,
1311            project,
1312        }
1313    }
1314
1315    fn authenticated_request(&self, method: Method, url: Url) -> RequestBuilder {
1316        self.client
1317            .request(method, url)
1318            .basic_auth(&self.user, Some(&self.pass))
1319    }
1320
1321    async fn send_with_error(request: RequestBuilder) -> Result<Response> {
1322        let response = request.send().await?;
1323
1324        match response.error_for_status_ref() {
1325            Ok(_) => Ok(response),
1326            Err(e) => {
1327                if let Some(status) = e.status() {
1328                    if status.is_client_error() {
1329                        let data = response.text().await?;
1330                        let error = quick_xml::de::from_str(&data)?;
1331                        Err(Error::ApiError(error))
1332                    } else {
1333                        Err(e.into())
1334                    }
1335                } else {
1336                    Err(e.into())
1337                }
1338            }
1339        }
1340    }
1341
1342    async fn request<T: DeserializeOwned + std::fmt::Debug>(&self, url: Url) -> Result<T> {
1343        let data = Self::send_with_error(self.authenticated_request(Method::GET, url))
1344            .await?
1345            .text()
1346            .await?;
1347        quick_xml::de::from_str(&data).map_err(|e| e.into())
1348    }
1349
1350    async fn post_request<T: DeserializeOwned + std::fmt::Debug>(&self, url: Url) -> Result<T> {
1351        let data = Self::send_with_error(self.authenticated_request(Method::POST, url))
1352            .await?
1353            .text()
1354            .await?;
1355        quick_xml::de::from_str(&data).map_err(|e| e.into())
1356    }
1357}