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