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 #[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 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 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}