Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion clients/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ publish = true

[dependencies]
async-compression = { version = "0.4.27", features = ["tokio", "zstd"] }
async-stream = "0.3.6"
base64 = "0.22.1"
bytes = { workspace = true }
futures-util = { workspace = true }
infer = { version = "0.19.0", default-features = false }
jsonwebtoken = { workspace = true }
multer = "3.1.0"
objectstore-types = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
reqwest = { workspace = true, features = ["json", "stream", "multipart"] }
sentry-core = { version = ">=0.41", features = ["client"] }
serde = { workspace = true }
thiserror = { workspace = true }
# See https://github.com/getsentry/relay/blob/9c491a185a289e09ee9d3f56d89bd9d1fa71d815/Cargo.toml#L211
tokio = "1.45.0"
tokio-util = { workspace = true, features = ["io"] }
url = "2.5.7"
uuid = { workspace = true }

[dev-dependencies]
objectstore-test = { workspace = true }
Expand Down
34 changes: 34 additions & 0 deletions clients/rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,40 @@ async fn example() -> Result<()> {
}
```

### Many API

The Many API allows you to enqueue multiple requests that the client can execute using Objectstore's batch endpoint, minimizing network overhead.

```rust
use objectstore_client::{Client, Usecase, OperationResult, Result};

async fn example_batch() -> Result<()> {
let client = Client::new("http://localhost:8888/")?;
let session = Usecase::new("attachments")
.for_project(42, 1337)
.session(&client)?;

let results: Vec<_> = session
.many()
.push(session.put("file1 contents").key("file1"))
.push(session.put("file2 contents").key("file2"))
.push(session.put("file3 contents").key("file3"))
.send()
.await?
.into_iter()
.collect();

for result in results {
match result {
// ...
_ => {},
}
}

Ok(())
}
```

See the [API docs](https://getsentry.github.io/objectstore/rust/objectstore_client/) for more in-depth documentation.

## License
Expand Down
42 changes: 33 additions & 9 deletions clients/rust/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::time::Duration;
use bytes::Bytes;
use futures_util::stream::BoxStream;
use objectstore_types::{Compression, ExpirationPolicy, scope};
use reqwest::RequestBuilder;
use url::Url;

use crate::auth::TokenGenerator;
Expand Down Expand Up @@ -408,30 +409,53 @@ impl Session {
url
}

pub(crate) fn request(
&self,
method: reqwest::Method,
object_key: &str,
) -> crate::Result<reqwest::RequestBuilder> {
let url = self.object_url(object_key);
fn batch_url(&self) -> Url {
let mut url = self.client.service_url.clone();

let mut builder = self.client.reqwest.request(method, url);
// `path_segments_mut` can only error if the url is cannot-be-a-base,
// and we check that in `ClientBuilder::new`, therefore this will never panic.
let mut segments = url.path_segments_mut().unwrap();
segments
.push("v1")
.push("objects:batch")
.push(self.scope.usecase().name())
.push(&self.scope.scopes.as_api_path().to_string())
.push(""); // trailing slash
drop(segments);

url
}

fn prepare_builder(&self, mut builder: RequestBuilder) -> crate::Result<RequestBuilder> {
if let Some(token_generator) = &self.client.token_generator {
let token = token_generator.sign_for_scope(&self.scope)?;
builder = builder.bearer_auth(token);
}

if self.client.propagate_traces {
let trace_headers =
sentry_core::configure_scope(|scope| Some(scope.iter_trace_propagation_headers()));
for (header_name, value) in trace_headers.into_iter().flatten() {
builder = builder.header(header_name, value);
}
}

Ok(builder)
}

pub(crate) fn request(
&self,
method: reqwest::Method,
object_key: &str,
) -> crate::Result<RequestBuilder> {
let url = self.object_url(object_key);
let builder = self.client.reqwest.request(method, url);
self.prepare_builder(builder)
}

pub(crate) fn batch_request(&self) -> crate::Result<RequestBuilder> {
let url = self.batch_url();
let builder = self.client.reqwest.post(url);
self.prepare_builder(builder)
}
}

#[cfg(test)]
Expand Down
6 changes: 3 additions & 3 deletions clients/rust/src/delete.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::Session;
use crate::{ObjectKey, Session};

/// The result from a successful [`delete()`](Session::delete) call.
pub type DeleteResponse = ();
Expand All @@ -16,8 +16,8 @@ impl Session {
/// A [`delete`](Session::delete) request builder.
#[derive(Debug)]
pub struct DeleteBuilder {
session: Session,
key: String,
pub(crate) session: Session,
pub(crate) key: ObjectKey,
}

impl DeleteBuilder {
Expand Down
17 changes: 17 additions & 0 deletions clients/rust/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ pub enum Error {
/// The URL error message.
message: String,
},
/// Error when parsing a multipart response.
#[error(transparent)]
Multipart(#[from] multer::Error),
/// Error when the server returned a malformed response.
#[error("{0}")]
MalformedResponse(String),
/// Error when parsing a batch response part.
#[error("batch response error: {0}")]
BatchResponse(String),
/// Error that indicates failure of an individual operation in a `many` request.
#[error("operation error (HTTP status code {status}): {message}")]
OperationError {
/// The HTTP status code corresponding to the status of the operation.
status: u16,
/// The error message.
message: String,
},
}

/// A convenience alias that defaults our [`Error`] type.
Expand Down
8 changes: 4 additions & 4 deletions clients/rust/src/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tokio_util::io::{ReaderStream, StreamReader};

pub use objectstore_types::Compression;

use crate::{ClientStream, Session};
use crate::{ClientStream, ObjectKey, Session};

/// The result from a successful [`get()`](Session::get) call.
///
Expand Down Expand Up @@ -58,9 +58,9 @@ impl Session {
/// A [`get`](Session::get) request builder.
#[derive(Debug)]
pub struct GetBuilder {
session: Session,
key: String,
decompress: bool,
pub(crate) session: Session,
pub(crate) key: ObjectKey,
pub(crate) decompress: bool,
}

impl GetBuilder {
Expand Down
5 changes: 5 additions & 0 deletions clients/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ mod client;
mod delete;
mod error;
mod get;
mod many;
mod put;
pub mod utils;

pub use objectstore_types::{Compression, ExpirationPolicy};

/// A key that uniquely identifies an object within its usecase and scopes.
pub type ObjectKey = String;

pub use auth::*;
pub use client::*;
pub use delete::*;
pub use error::*;
pub use get::*;
pub use many::*;
pub use put::*;
Loading