objectstore_client/
client.rs

1use std::io;
2use std::sync::Arc;
3
4use bytes::Bytes;
5use futures_util::stream::BoxStream;
6use objectstore_types::ExpirationPolicy;
7use reqwest::header::HeaderName;
8
9pub use objectstore_types::{Compression, PARAM_SCOPE, PARAM_USECASE};
10
11const USER_AGENT: &str = concat!("objectstore-client/", env!("CARGO_PKG_VERSION"));
12
13/// Service for storing and retrieving objects.
14///
15/// The Service contains the base configuration to connect to a service.
16/// It has to be further initialized with credentials using the
17/// [`for_organization`](Self::for_organization) and
18/// [`for_project`](Self::for_project) functions.
19#[derive(Debug)]
20pub struct ClientBuilder {
21    service_url: Arc<str>,
22    client: reqwest::Client,
23    propagate_traces: bool,
24
25    usecase: Arc<str>,
26    default_compression: Compression,
27    default_expiration_policy: ExpirationPolicy,
28}
29
30impl ClientBuilder {
31    /// Creates a new [`ClientBuilder`].
32    ///
33    /// This service instance is configured to target the given `service_url`.
34    /// It is also scoped for the given `usecase`.
35    ///
36    /// In order to get or put objects, one has to create a [`Client`] using the
37    /// [`for_organization`](Self::for_organization) function.
38    pub fn new(service_url: &str, usecase: &str) -> anyhow::Result<Self> {
39        let client = reqwest::Client::builder()
40            .user_agent(USER_AGENT)
41            // hickory-dns: Controlled by the `reqwest/hickory-dns` feature flag
42            // we are dealing with de/compression ourselves:
43            .no_brotli()
44            .no_deflate()
45            .no_gzip()
46            .no_zstd()
47            .build()?;
48
49        Ok(Self {
50            service_url: service_url.trim_end_matches('/').into(),
51            client,
52            propagate_traces: false,
53
54            usecase: usecase.into(),
55            default_compression: Compression::Zstd,
56            default_expiration_policy: ExpirationPolicy::Manual,
57        })
58    }
59
60    /// This changes the default compression used for uploads.
61    pub fn default_compression(mut self, compression: Compression) -> Self {
62        self.default_compression = compression;
63        self
64    }
65
66    /// This sets a default expiration policy used for uploads.
67    pub fn default_expiration_policy(mut self, expiration_policy: ExpirationPolicy) -> Self {
68        self.default_expiration_policy = expiration_policy;
69        self
70    }
71
72    /// This changes whether the `sentry-trace` header will be sent to Objectstore
73    /// to take advantage of Sentry's distributed tracing.
74    pub fn with_distributed_tracing(mut self, propagate_traces: bool) -> Self {
75        self.propagate_traces = propagate_traces;
76        self
77    }
78
79    fn make_client(&self, scope: String) -> Client {
80        Client {
81            service_url: self.service_url.clone(),
82            http: self.client.clone(),
83            propagate_traces: self.propagate_traces,
84
85            usecase: self.usecase.clone(),
86            scope,
87            default_compression: self.default_compression,
88            default_expiration_policy: self.default_expiration_policy,
89        }
90    }
91
92    /// Create a new [`Client`] and sets its `scope` based on the provided organization.
93    pub fn for_organization(&self, organization_id: u64) -> Client {
94        let scope = format!("org.{organization_id}");
95        self.make_client(scope)
96    }
97
98    /// Create a new [`Client`] and sets its `scope` based on the provided organization
99    /// and project.
100    pub fn for_project(&self, organization_id: u64, project_id: u64) -> Client {
101        let scope = format!("org.{organization_id}/proj.{project_id}");
102        self.make_client(scope)
103    }
104}
105
106/// A scoped objectstore client that can access objects in a specific use case and scope.
107#[derive(Debug)]
108pub struct Client {
109    pub(crate) http: reqwest::Client,
110    pub(crate) service_url: Arc<str>,
111    propagate_traces: bool,
112
113    pub(crate) usecase: Arc<str>,
114
115    /// The scope that this client operates within.
116    ///
117    /// Scopes are expected to be serialized ordered lists of key/value pairs. Each
118    /// pair is serialized with a `.` character between the key and value, and with
119    /// a `/` character between each pair. For example:
120    /// - `org.123/proj.456`
121    /// - `state.washington/city.seattle`
122    ///
123    /// It is recommended that both keys and values be restricted to alphanumeric
124    /// characters.
125    pub(crate) scope: String,
126    pub(crate) default_compression: Compression,
127    pub(crate) default_expiration_policy: ExpirationPolicy,
128}
129
130/// The type of [`Stream`](futures_util::Stream) to be used for a PUT request.
131pub type ClientStream = BoxStream<'static, io::Result<Bytes>>;
132
133impl Client {
134    pub(crate) fn request<U: reqwest::IntoUrl>(
135        &self,
136        method: reqwest::Method,
137        uri: U,
138    ) -> anyhow::Result<reqwest::RequestBuilder> {
139        let mut builder = self.http.request(method, uri).query(&[
140            (PARAM_SCOPE, self.scope.as_ref()),
141            (PARAM_USECASE, self.usecase.as_ref()),
142        ]);
143
144        if self.propagate_traces {
145            let trace_headers =
146                sentry::configure_scope(|scope| Some(scope.iter_trace_propagation_headers()));
147            for (header_name, value) in trace_headers.into_iter().flatten() {
148                builder = builder.header(HeaderName::try_from(header_name)?, value);
149            }
150        }
151
152        Ok(builder)
153    }
154
155    /// Deletes the object with the given `id`.
156    pub async fn delete(&self, id: &str) -> anyhow::Result<()> {
157        let delete_url = format!("{}/v1/{id}", self.service_url);
158
159        let _response = self
160            .request(reqwest::Method::DELETE, delete_url)?
161            .send()
162            .await?;
163
164        Ok(())
165    }
166}