rp_supabase_storage/
client.rs1use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
2use reqwest::{Method, RequestBuilder, Response};
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use url::Url;
6
7use crate::error::{ApiErrorBody, StorageError};
8use crate::types::{BucketInfo, BucketOptions};
9use crate::{Bucket, path};
10
11fn same_origin_redirects() -> reqwest::redirect::Policy {
14 reqwest::redirect::Policy::custom(|attempt| {
15 let same_origin = attempt
16 .previous()
17 .last()
18 .is_some_and(|prev| prev.origin() == attempt.url().origin());
19 if !same_origin {
20 attempt.stop()
21 } else if attempt.previous().len() > 10 {
22 attempt.error("too many redirects")
23 } else {
24 attempt.follow()
25 }
26 })
27}
28
29#[derive(Debug, Clone)]
33pub struct StorageClient {
34 http: reqwest::Client,
35 base: Url,
36 headers: HeaderMap,
37}
38
39#[derive(Serialize)]
40struct BucketBody<'a> {
41 id: &'a str,
42 name: &'a str,
43 #[serde(flatten)]
44 options: &'a BucketOptions,
45}
46
47impl StorageClient {
48 pub fn new(project_url: &Url, api_key: &str) -> Result<Self, StorageError> {
60 let http = reqwest::Client::builder()
61 .redirect(same_origin_redirects())
62 .build()?;
63 Self::new_with_client(project_url, api_key, http)
64 }
65
66 pub fn new_with_client(
72 project_url: &Url,
73 api_key: &str,
74 http: reqwest::Client,
75 ) -> Result<Self, StorageError> {
76 let base = storage_base(project_url)?;
77 let mut headers = HeaderMap::new();
78 headers.insert("apikey", sensitive(api_key)?);
79 if !(api_key.starts_with("sb_publishable_") || api_key.starts_with("sb_secret_")) {
80 headers.insert(AUTHORIZATION, sensitive(&format!("Bearer {api_key}"))?);
81 }
82 Ok(Self {
83 http,
84 base,
85 headers,
86 })
87 }
88
89 pub fn with_access_token(&self, token: &str) -> Result<Self, StorageError> {
96 let mut next = self.clone();
97 next.headers
98 .insert(AUTHORIZATION, sensitive(&format!("Bearer {token}"))?);
99 Ok(next)
100 }
101
102 pub fn with_project_url(&self, project_url: &Url) -> Result<Self, StorageError> {
109 Ok(Self {
110 http: self.http.clone(),
111 base: storage_base(project_url)?,
112 headers: self.headers.clone(),
113 })
114 }
115
116 #[must_use]
118 pub const fn from<'a>(&'a self, bucket: &'a str) -> Bucket<'a> {
119 Bucket::new(self, bucket)
120 }
121
122 pub async fn list_buckets(&self) -> Result<Vec<BucketInfo>, StorageError> {
128 Self::json(self.request(Method::GET, self.url(["bucket"]))).await
129 }
130
131 pub async fn get_bucket(&self, id: &str) -> Result<BucketInfo, StorageError> {
137 let url = self.url(["bucket", path::bucket(id)?]);
138 Self::json(self.request(Method::GET, url)).await
139 }
140
141 pub async fn create_bucket(
147 &self,
148 id: &str,
149 options: &BucketOptions,
150 ) -> Result<(), StorageError> {
151 let body = BucketBody {
152 id: path::bucket(id)?,
153 name: id,
154 options,
155 };
156 Self::send(self.request(Method::POST, self.url(["bucket"])).json(&body)).await?;
157 Ok(())
158 }
159
160 pub async fn update_bucket(
166 &self,
167 id: &str,
168 options: &BucketOptions,
169 ) -> Result<(), StorageError> {
170 let url = self.url(["bucket", path::bucket(id)?]);
171 let body = BucketBody {
172 id,
173 name: id,
174 options,
175 };
176 Self::send(self.request(Method::PUT, url).json(&body)).await?;
177 Ok(())
178 }
179
180 pub async fn empty_bucket(&self, id: &str) -> Result<(), StorageError> {
186 let url = self.url(["bucket", path::bucket(id)?, "empty"]);
187 Self::send(
188 self.request(Method::POST, url)
189 .json(&serde_json::Map::new()),
190 )
191 .await?;
192 Ok(())
193 }
194
195 pub async fn delete_bucket(&self, id: &str) -> Result<(), StorageError> {
202 let url = self.url(["bucket", path::bucket(id)?]);
203 Self::send(self.request(Method::DELETE, url)).await?;
204 Ok(())
205 }
206
207 pub(crate) fn url<'a, I: IntoIterator<Item = &'a str>>(&self, segments: I) -> Url {
209 let mut url = self.base.clone();
210 if let Ok(mut path) = url.path_segments_mut() {
212 path.pop_if_empty().extend(segments);
213 }
214 url
215 }
216
217 pub(crate) fn request(&self, method: Method, url: Url) -> RequestBuilder {
218 self.http.request(method, url).headers(self.headers.clone())
219 }
220
221 pub(crate) async fn send(request: RequestBuilder) -> Result<Response, StorageError> {
222 let response = request.send().await?;
223 let status = response.status();
224 if status.is_success() {
225 return Ok(response);
226 }
227 let body = ApiErrorBody::decode(response.text().await?);
228 Err(StorageError::Api { status, body })
229 }
230
231 pub(crate) async fn json<T: DeserializeOwned>(
232 request: RequestBuilder,
233 ) -> Result<T, StorageError> {
234 let bytes = Self::send(request).await?.bytes().await?;
235 Ok(serde_json::from_slice(&bytes)?)
236 }
237}
238
239fn storage_base(project_url: &Url) -> Result<Url, StorageError> {
240 let base = project_url.join("storage/v1")?;
241 if base.cannot_be_a_base() {
242 return Err(StorageError::InvalidBaseUrl);
243 }
244 Ok(base)
245}
246
247fn sensitive(value: &str) -> Result<HeaderValue, StorageError> {
248 let mut header = HeaderValue::from_str(value)?;
249 header.set_sensitive(true);
250 Ok(header)
251}