cf_r2_sdk/lib.rs
1//! # Unofficial Cloudflare R2 SDK
2//!
3//! This is the "Unofficial Cloudflare R2 SDK".
4//!
5//! It can upload, download, and delete binary data or files to Cloudflare R2.
6//!
7//! This crate is based on [cloudflare-r2-rs](https://crates.io/crates/cloudflare-r2-rs) (License: [Apache-2.0](https://choosealicense.com/licenses/apache-2.0/), Owner: [milen-denev](https://github.com/milen-denev)) and [r2sync](https://crates.io/crates/r2sync) (License: [MIT](https://github.com/Songmu/r2sync/blob/main/LICENSE), Owner: [Songmu](https://github.com/Songmu)).
8//!
9//! <div class="warning">
10//!
11//! date: 2025-01-18
12//!
13//! This crate is <strong>solved</strong> [this problem](https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj) by adding the following S3Client config.
14//!
15//! requestChecksumCalculation: "WHEN_REQUIRED",
16//! responseChecksumValidation: "WHEN_REQUIRED",
17//!
18//! Reference: [https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/](https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/)
19//!
20//! </div>
21//!
22//! # Struct
23//!
24//! [builder::Builder](https://docs.rs/cf-r2-sdk/latest/cf_r2_sdk/builder/struct.Builder.html)
25//!
26//! [operator::Operator](https://docs.rs/cf-r2-sdk/latest/cf_r2_sdk/operator/struct.Operator.html)
27//!
28//! # How to use
29//!
30//! ### 1. Create a aws_sdk_s3::Client object
31//!
32//! Set the "bucket name", "access key id", "secret access key", "endpoint url", and "region".
33//!
34//! Default value of region is "auto" (region is option field).
35//!
36//! ```
37//! use cf_r2_sdk::builder::Builder;
38//! use cf_r2_sdk::error::Error;
39//! use dotenvy::dotenv;
40//! use std::env;
41//!
42//! #[tokio::main(flavor = "current_thread")]
43//! async fn main() -> Result<(), Error> {
44//! // load .env file
45//! dotenv().expect(".env file not found.");
46//! // insert a environment variable
47//! let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
48//! let endpoint_url: String =
49//! env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
50//! let access_key_id: String =
51//! env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
52//! let secret_access_key: String =
53//! env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
54//! let region: String = env::var("REGION").expect("REGION not found in .env file.");
55//!
56//! let object: cf_r2_sdk::operator::Operator = Builder::new()
57//! .set_bucket_name(bucket_name)
58//! .set_access_key_id(access_key_id)
59//! .set_secret_access_key(secret_access_key)
60//! .set_endpoint(endpoint_url)
61//! .set_region(region)
62//! .create_client_result()?;
63//! Ok(())
64//! }
65//! ```
66//!
67//! ### 2. Operate R2 object strage
68//!
69//! #### Upload binary data
70//!
71//!
72//! ```
73//! use cf_r2_sdk::builder::Builder;
74//! use cf_r2_sdk::error::Error;
75//! use dotenvy::dotenv;
76//! use std::env;
77//!
78//! #[tokio::main(flavor = "current_thread")]
79//! async fn main() -> Result<(), Error> {
80//! // load .env file
81//! dotenv().expect(".env file not found.");
82//! // insert a environment variable
83//! let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
84//! let endpoint_url: String =
85//! env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
86//! let access_key_id: String =
87//! env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
88//! let secret_access_key: String =
89//! env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
90//! let region: String = env::var("REGION").expect("REGION not found in .env file.");
91//!
92//! let object: cf_r2_sdk::operator::Operator = Builder::new()
93//! .set_bucket_name(bucket_name)
94//! .set_access_key_id(access_key_id)
95//! .set_secret_access_key(secret_access_key)
96//! .set_endpoint(endpoint_url)
97//! .set_region(region)
98//! .create_client_result()?;
99//!
100//! // upload binary data
101//! object
102//! .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
103//! .await?;
104//! Ok(())
105//! }
106//! ```
107//!
108//! #### upload file
109//!
110//! ```
111//! use cf_r2_sdk::builder::Builder;
112//! use cf_r2_sdk::error::Error;
113//! use dotenvy::dotenv;
114//! use std::env;
115//!
116//! #[tokio::main(flavor = "current_thread")]
117//! async fn main() -> Result<(), Error> {
118//! // load .env file
119//! dotenv().expect(".env file not found.");
120//! // insert a environment variable
121//! let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
122//! let endpoint_url: String =
123//! env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
124//! let access_key_id: String =
125//! env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
126//! let secret_access_key: String =
127//! env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
128//! let region: String = env::var("REGION").expect("REGION not found in .env file.");
129//!
130//! let object: cf_r2_sdk::operator::Operator = Builder::new()
131//! .set_bucket_name(bucket_name)
132//! .set_access_key_id(access_key_id)
133//! .set_secret_access_key(secret_access_key)
134//! .set_endpoint(endpoint_url)
135//! .set_region(region)
136//! .create_client_result()?;
137//!
138//! // upload file
139//! object
140//! .upload_file("sample.jpg", "image/jpeg", "data/sample.jpg", None)
141//! .await?;
142//! Ok(())
143//! }
144//! ```
145//!
146//! #### download binary data
147//!
148//! ```
149//! use cf_r2_sdk::builder::Builder;
150//! use cf_r2_sdk::error::Error;
151//! use dotenvy::dotenv;
152//! use std::env;
153//!
154//! #[tokio::main(flavor = "current_thread")]
155//! async fn main() -> Result<(), Error> {
156//! // load .env file
157//! dotenv().expect(".env file not found.");
158//! // insert a environment variable
159//! let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
160//! let endpoint_url: String =
161//! env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
162//! let access_key_id: String =
163//! env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
164//! let secret_access_key: String =
165//! env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
166//! let region: String = env::var("REGION").expect("REGION not found in .env file.");
167//!
168//! let object: cf_r2_sdk::operator::Operator = Builder::new()
169//! .set_bucket_name(bucket_name)
170//! .set_access_key_id(access_key_id)
171//! .set_secret_access_key(secret_access_key)
172//! .set_endpoint(endpoint_url)
173//! .set_region(region)
174//! .create_client_result()?;
175//!
176//! object
177//! .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
178//! .await?;
179//!
180//! // download binary data
181//! let bin: Vec<u8> = object.download("sample.txt").await?;
182//!
183//! println!("{:?}", bin);
184//! Ok(())
185//! }
186//! ```
187//!
188//! #### delete file
189//!
190//! ```
191//! use cf_r2_sdk::builder::Builder;
192//! use cf_r2_sdk::error::Error;
193//! use dotenvy::dotenv;
194//! use std::env;
195//!
196//! #[tokio::main(flavor = "current_thread")]
197//! async fn main() -> Result<(), Error> {
198//! // load .env file
199//! dotenv().expect(".env file not found.");
200//! // insert a environment variable
201//! let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
202//! let endpoint_url: String =
203//! env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
204//! let access_key_id: String =
205//! env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
206//! let secret_access_key: String =
207//! env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
208//! let region: String = env::var("REGION").expect("REGION not found in .env file.");
209//!
210//! let object: cf_r2_sdk::operator::Operator = Builder::new()
211//! .set_bucket_name(bucket_name)
212//! .set_access_key_id(access_key_id)
213//! .set_secret_access_key(secret_access_key)
214//! .set_endpoint(endpoint_url)
215//! .set_region(region)
216//! .create_client_result()?;
217//!
218//! object
219//! .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
220//! .await?;
221//!
222//! // delete file
223//! object
224//! .delete("sample.txt")
225//! .await?;
226//!
227//! Ok(())
228//! }
229//! ```
230//!
231//! #### get file names vector
232//!
233//! ```
234//! use cf_r2_sdk::builder::Builder;
235//! use cf_r2_sdk::error::Error;
236//! use dotenvy::dotenv;
237//! use std::env;
238//!
239//! #[tokio::main(flavor = "current_thread")]
240//! async fn main() -> Result<(), Error> {
241//! // load .env file
242//! dotenv().expect(".env file not found.");
243//! // insert a environment variable
244//! let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
245//! let endpoint_url: String =
246//! env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
247//! let access_key_id: String =
248//! env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
249//! let secret_access_key: String =
250//! env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
251//! let region: String = env::var("REGION").expect("REGION not found in .env file.");
252//!
253//! let object: cf_r2_sdk::operator::Operator = Builder::new()
254//! .set_bucket_name(bucket_name)
255//! .set_access_key_id(access_key_id)
256//! .set_secret_access_key(secret_access_key)
257//! .set_endpoint(endpoint_url)
258//! .set_region(region)
259//! .create_client_result()?;
260//!
261//! object
262//! .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
263//! .await?;
264//!
265//! // get file names vector
266//! let file_names: Vec<String> = object.list_objects().await?;
267//!
268//! for file_name in file_names {
269//! println!("{}", file_name);
270//! }
271//!
272//! Ok(())
273//! }
274//! ```
275
276pub mod builder;
277pub mod error;
278pub mod operator;
279
280#[cfg(test)]
281mod tests {
282 use crate::error::Error;
283
284 use super::*;
285 use builder::Builder;
286 use dotenvy::dotenv;
287 use std::env;
288 use tokio::{fs::File, io::AsyncReadExt};
289
290 #[tokio::test]
291 async fn local_test_1_upload_and_download_binary() -> Result<(), Error> {
292 // load .env file
293 dotenv().expect(".env file not found.");
294 // insert a environment variable
295 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
296 let endpoint_url: String =
297 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
298 let access_key_id: String =
299 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
300 let secret_access_key: String =
301 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
302 let region: String = env::var("REGION").expect("REGION not found in .env file.");
303
304 let object = Builder::new()
305 .set_bucket_name(bucket_name)
306 .set_access_key_id(access_key_id)
307 .set_secret_access_key(secret_access_key)
308 .set_endpoint(endpoint_url)
309 .set_region(region)
310 .create_client_result()?;
311
312 object
313 .upload_binary("test.txt", "text/plain", b"Hello, World!", None)
314 .await?;
315
316 let bin = object.download("test.txt").await;
317 match bin {
318 Ok(bin) => assert_eq!(bin, b"Hello, World!"),
319 Err(e) => panic!("Error: {:?}", e),
320 }
321 Ok(())
322 }
323
324 #[tokio::test]
325 async fn local_test_2_upload_and_download_file() -> Result<(), Error> {
326 // load .env file
327 dotenv().expect(".env file not found.");
328 // insert a environment variable
329 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
330 let endpoint_url: String =
331 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
332 let access_key_id: String =
333 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
334 let secret_access_key: String =
335 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
336 let region: String = env::var("REGION").expect("REGION not found in .env file.");
337
338 let object = Builder::new()
339 .set_bucket_name(bucket_name)
340 .set_access_key_id(access_key_id)
341 .set_secret_access_key(secret_access_key)
342 .set_endpoint(endpoint_url)
343 .set_region(region)
344 .create_client_result()?;
345
346 let file_path = "./data/sample.jpg";
347 object
348 .upload_file("sample.jpg", "image/jpeg", file_path, None)
349 .await?;
350
351 let bin = object.download("sample.jpg").await;
352
353 //read file from local
354 let mut file = File::open(file_path).await.expect("Failed to open file");
355 let mut buffer = Vec::new();
356 file.read_to_end(&mut buffer)
357 .await
358 .expect("Failed to close file");
359
360 match bin {
361 Ok(bin) => assert_eq!(bin, buffer),
362 Err(e) => panic!("Error: {:?}", e),
363 }
364 Ok(())
365 }
366
367 #[tokio::test]
368 async fn local_test_3_upload_and_delete() -> Result<(), Error> {
369 // load .env file
370 dotenv().expect(".env file not found.");
371 // insert a environment variable
372 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
373 let endpoint_url: String =
374 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
375 let access_key_id: String =
376 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
377 let secret_access_key: String =
378 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
379 let region: String = env::var("REGION").expect("REGION not found in .env file.");
380
381 let object = Builder::new()
382 .set_bucket_name(bucket_name)
383 .set_access_key_id(access_key_id)
384 .set_secret_access_key(secret_access_key)
385 .set_endpoint(endpoint_url)
386 .set_region(region)
387 .create_client_result()?;
388
389 object
390 .upload_binary("text.txt", "text/plain", b"Hello, World!", None)
391 .await?;
392
393 object.delete("text.txt").await?;
394
395 let bin: Result<Vec<u8>, error::OperationError> = object.download("text.txt").await;
396 if bin.is_ok() {
397 panic!("Error: {:?}", bin)
398 }
399 Ok(())
400 }
401
402 #[tokio::test]
403 async fn local_test_4_list_objects() -> Result<(), Error> {
404 // load .env file
405 dotenv().expect(".env file not found.");
406 // insert a environment variable
407 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
408 let endpoint_url: String =
409 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
410 let access_key_id: String =
411 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
412 let secret_access_key: String =
413 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
414 let region: String = env::var("REGION").expect("REGION not found in .env file.");
415
416 let object = Builder::new()
417 .set_bucket_name(bucket_name)
418 .set_access_key_id(access_key_id)
419 .set_secret_access_key(secret_access_key)
420 .set_endpoint(endpoint_url)
421 .set_region(region)
422 .create_client_result()?;
423
424 let _data = object.list_objects().await?;
425
426 Ok(())
427 }
428
429 //* Actions Test *//
430 #[tokio::test]
431 #[ignore]
432 async fn actions_test_1_upload_and_download_binary() -> Result<(), Error> {
433 // insert a environment variable
434 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
435 let endpoint_url: String =
436 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
437 let access_key_id: String =
438 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
439 let secret_access_key: String =
440 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
441 let region: String = env::var("REGION").expect("REGION not found in .env file.");
442
443 let object = Builder::new()
444 .set_bucket_name(bucket_name)
445 .set_access_key_id(access_key_id)
446 .set_secret_access_key(secret_access_key)
447 .set_endpoint(endpoint_url)
448 .set_region(region)
449 .create_client_result()?;
450
451 object
452 .upload_binary("test.txt", "text/plain", b"Hello, World!", None)
453 .await?;
454
455 let bin = object.download("test.txt").await;
456 match bin {
457 Ok(bin) => assert_eq!(bin, b"Hello, World!"),
458 Err(e) => panic!("Error: {:?}", e),
459 }
460 Ok(())
461 }
462
463 #[tokio::test]
464 #[ignore]
465 async fn actions_test_2_upload_and_download_file() -> Result<(), Error> {
466 // insert a environment variable
467 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
468 let endpoint_url: String =
469 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
470 let access_key_id: String =
471 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
472 let secret_access_key: String =
473 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
474 let region: String = env::var("REGION").expect("REGION not found in .env file.");
475
476 let object = Builder::new()
477 .set_bucket_name(bucket_name)
478 .set_access_key_id(access_key_id)
479 .set_secret_access_key(secret_access_key)
480 .set_endpoint(endpoint_url)
481 .set_region(region)
482 .create_client_result()?;
483
484 let file_path = "./data/sample.jpg";
485 object
486 .upload_file("sample.jpg", "image/jpeg", file_path, None)
487 .await?;
488
489 let bin = object.download("sample.jpg").await;
490
491 //read file from local
492 let mut file = File::open(file_path).await.expect("Failed to open file");
493 let mut buffer = Vec::new();
494 file.read_to_end(&mut buffer)
495 .await
496 .expect("Failed to close file");
497
498 match bin {
499 Ok(bin) => assert_eq!(bin, buffer),
500 Err(e) => panic!("Error: {:?}", e),
501 }
502 Ok(())
503 }
504
505 #[tokio::test]
506 #[ignore]
507 async fn actions_test_3_upload_and_delete() -> Result<(), Error> {
508 // insert a environment variable
509 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
510 let endpoint_url: String =
511 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
512 let access_key_id: String =
513 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
514 let secret_access_key: String =
515 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
516 let region: String = env::var("REGION").expect("REGION not found in .env file.");
517
518 let object = Builder::new()
519 .set_bucket_name(bucket_name)
520 .set_access_key_id(access_key_id)
521 .set_secret_access_key(secret_access_key)
522 .set_endpoint(endpoint_url)
523 .set_region(region)
524 .create_client_result()?;
525
526 object
527 .upload_binary("text.txt", "text/plain", b"Hello, World!", None)
528 .await?;
529
530 object.delete("text.txt").await?;
531
532 let bin: Result<Vec<u8>, error::OperationError> = object.download("text.txt").await;
533 if bin.is_ok() {
534 panic!("Error: {:?}", bin)
535 }
536 Ok(())
537 }
538
539 #[tokio::test]
540 #[ignore]
541 async fn actions_test_4_list_objects() -> Result<(), Error> {
542 // insert a environment variable
543 let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
544 let endpoint_url: String =
545 env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
546 let access_key_id: String =
547 env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
548 let secret_access_key: String =
549 env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
550 let region: String = env::var("REGION").expect("REGION not found in .env file.");
551
552 let object = Builder::new()
553 .set_bucket_name(bucket_name)
554 .set_access_key_id(access_key_id)
555 .set_secret_access_key(secret_access_key)
556 .set_endpoint(endpoint_url)
557 .set_region(region)
558 .create_client_result()?;
559
560 let _data = object.list_objects().await?;
561
562 Ok(())
563 }
564}