1use std::io::Read;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use aws_config::Region;
10use aws_config::default_provider::credentials::DefaultCredentialsChain;
11use aws_config::meta::region::RegionProviderChain;
12pub use aws_sdk_s3::Client as S3Client;
13use aws_sdk_s3::config::{Builder as S3ClientBuilder, Credentials, ProvideCredentials};
14use aws_sdk_s3::primitives::{ByteStream, SdkBody};
15use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart, Object};
16#[cfg(target_os = "windows")]
17use path_slash::PathExt as _;
18use remotefs::fs::{Metadata, ReadStream, UnixPex, Welcome, WriteStream};
19use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs, RemoteResult};
20use tokio::runtime::Runtime;
21
22use super::object::S3Object;
23use crate::utils::path as path_utils;
24
25const MIN_MULTIPART_UPLOAD_SIZE: usize = 5 * 1024 * 1024; pub struct AwsS3Fs {
29 client: Option<S3Client>,
30 runtime: Arc<Runtime>,
31 wrkdir: PathBuf,
32 bucket_name: String,
34 region: Option<String>,
36 endpoint: Option<String>,
38 profile: Option<String>,
39 access_key: Option<String>,
40 secret_key: Option<String>,
41 security_token: Option<String>,
42 session_token: Option<String>,
43 new_path_style: bool,
45}
46
47#[derive(Debug)]
48pub enum RemoteFsCredentials {
49 Default(DefaultCredentialsChain),
50 User(Credentials),
51}
52
53impl ProvideCredentials for RemoteFsCredentials {
54 fn fallback_on_interrupt(&self) -> Option<Credentials> {
55 match self {
56 Self::Default(c) => c.fallback_on_interrupt(),
57 Self::User(c) => c.fallback_on_interrupt(),
58 }
59 }
60
61 fn provide_credentials<'a>(
62 &'a self,
63 ) -> aws_credential_types::provider::future::ProvideCredentials<'a>
64 where
65 Self: 'a,
66 {
67 match self {
68 Self::Default(c) => c.provide_credentials(),
69 Self::User(c) => c.provide_credentials(),
70 }
71 }
72}
73
74impl AwsS3Fs {
75 pub fn new<S: AsRef<str>>(bucket: S, runtime: &Arc<Runtime>) -> Self {
77 Self {
78 client: None,
79 runtime: runtime.clone(),
80 wrkdir: PathBuf::from("/"),
81 bucket_name: bucket.as_ref().to_string(),
82 region: None,
83 endpoint: None,
84 profile: None,
85 access_key: None,
86 secret_key: None,
87 security_token: None,
88 session_token: None,
89 new_path_style: false,
90 }
91 }
92
93 pub fn region<S: AsRef<str>>(mut self, region: S) -> Self {
95 self.region = Some(region.as_ref().to_string());
96 self
97 }
98
99 pub fn endpoint<S: AsRef<str>>(mut self, endpoint: S) -> Self {
102 self.endpoint = Some(endpoint.as_ref().to_string());
103 self
104 }
105
106 pub fn profile<S: AsRef<str>>(mut self, profile: S) -> Self {
108 self.profile = Some(profile.as_ref().to_string());
109 self
110 }
111
112 pub fn new_path_style(mut self, new_path_style: bool) -> Self {
114 self.new_path_style = new_path_style;
115 self
116 }
117
118 pub fn access_key<S: AsRef<str>>(mut self, key: S) -> Self {
121 self.access_key = Some(key.as_ref().to_string());
122 self
123 }
124
125 pub fn secret_access_key<S: AsRef<str>>(mut self, key: S) -> Self {
128 self.secret_key = Some(key.as_ref().to_string());
129 self
130 }
131
132 pub fn security_token<S: AsRef<str>>(mut self, key: S) -> Self {
135 self.security_token = Some(key.as_ref().to_string());
136 self
137 }
138
139 pub fn session_token<S: AsRef<str>>(mut self, key: S) -> Self {
142 self.session_token = Some(key.as_ref().to_string());
143 self
144 }
145
146 pub fn client(&self) -> Option<&S3Client> {
150 self.client.as_ref()
151 }
152
153 fn list_objects(&self, p: &Path, list_dir: bool) -> RemoteResult<Vec<S3Object>> {
157 let key: String = Self::fmt_path(p, list_dir);
159 debug!("Query list directory {}; key: {}", p.display(), key);
160 self.query_objects(key, true)
161 }
162
163 fn stat_object(&self, p: &Path) -> RemoteResult<S3Object> {
165 let key: String = Self::fmt_path(p, false);
166 debug!("Query stat object {}; key: {}", p.display(), key);
167 let objects = self.query_objects(key, false)?;
168 let absol: PathBuf = path_utils::absolutize(Path::new("/"), p);
170 match objects
172 .into_iter()
173 .find(|x| x.path.as_path() == absol.as_path())
174 {
175 Some(obj) => Ok(obj),
176 None => Err(RemoteError::new_ex(
177 RemoteErrorType::NoSuchFileOrDirectory,
178 format!("{}: No such file or directory", p.display()),
179 )),
180 }
181 }
182
183 fn query_objects(
185 &self,
186 key: String,
187 only_direct_children: bool,
188 ) -> RemoteResult<Vec<S3Object>> {
189 debug!("query objects with prefix: '{key}'");
190
191 let fut = self
192 .client
193 .as_ref()
194 .unwrap()
195 .list_objects_v2()
196 .bucket(self.bucket_name.as_str())
197 .prefix(key.as_str())
198 .send();
199 let results = self.runtime.block_on(fut);
200 match results {
201 Ok(entries) => {
202 let Some(contents) = entries.contents else {
203 debug!("No objects found at key {key}",);
204 return Ok(vec![]);
205 };
206
207 let objects: Vec<S3Object> = contents
208 .into_iter()
209 .filter(|object| {
210 if only_direct_children {
211 Self::list_object_should_be_kept(object, key.as_str())
212 } else {
213 true
214 }
215 })
216 .map(S3Object::from)
217 .collect();
218
219 debug!("Found objects: {:?}", objects);
220 Ok(objects)
221 }
222 Err(e) => Err(RemoteError::new_ex(RemoteErrorType::StatFailed, e)),
223 }
224 }
225
226 fn list_object_should_be_kept(obj: &Object, dir: &str) -> bool {
231 Self::is_direct_child(obj.key.as_deref().unwrap_or_default(), dir)
232 }
233
234 fn is_direct_child(key: &str, parent: &str) -> bool {
236 key == format!("{}{}", parent, S3Object::object_name(key))
237 || key == format!("{}{}/", parent, S3Object::object_name(key))
238 }
239
240 fn resolve(&self, p: &Path) -> PathBuf {
242 path_utils::diff_paths(
243 path_utils::absolutize(self.wrkdir.as_path(), p),
244 Path::new("/"),
245 )
246 .unwrap_or_default()
247 }
248
249 fn fmt_path(p: &Path, is_dir: bool) -> String {
251 if p == Path::new("/") {
253 return "".to_string();
254 }
255 #[cfg(target_family = "unix")]
257 let is_absolute: bool = p.is_absolute();
258 #[cfg(target_family = "windows")]
260 let is_absolute: bool = p.display().to_string().starts_with('/');
261 let p: PathBuf = match is_absolute {
262 true => path_utils::diff_paths(p, Path::new("/")).unwrap_or_default(),
263 false => p.to_path_buf(),
264 };
265 #[cfg(target_family = "windows")]
267 let p: PathBuf = PathBuf::from(p.to_slash_lossy().to_string());
268 match is_dir {
270 true => {
271 let mut p: String = p.display().to_string();
272 if !p.ends_with('/') {
273 p.push('/');
274 }
275 p
276 }
277 false => p.to_string_lossy().to_string(),
278 }
279 }
280
281 fn check_connection(&mut self) -> RemoteResult<()> {
283 if self.is_connected() {
284 Ok(())
285 } else {
286 Err(RemoteError::new(RemoteErrorType::NotConnected))
287 }
288 }
289
290 fn is_anonymous(&self) -> bool {
292 self.access_key.is_none()
293 && self.secret_key.is_none()
294 && self.security_token.is_none()
295 && self.session_token.is_none()
296 }
297
298 fn load_credentials(&self, region: RegionProviderChain) -> RemoteResult<RemoteFsCredentials> {
300 if self.is_anonymous() {
301 Ok(RemoteFsCredentials::Default(self.runtime.block_on(
302 DefaultCredentialsChain::builder().region(region).build(),
303 )))
304 } else {
305 let Some(access_key) = self.access_key.as_ref() else {
306 return Err(RemoteError::new_ex(
307 RemoteErrorType::AuthenticationFailed,
308 "Access key not set",
309 ));
310 };
311 let Some(secret_key) = self.secret_key.as_ref() else {
312 return Err(RemoteError::new_ex(
313 RemoteErrorType::AuthenticationFailed,
314 "Secret key not set",
315 ));
316 };
317
318 Ok(RemoteFsCredentials::User(Credentials::new(
319 access_key,
320 secret_key,
321 self.session_token.clone(),
322 None,
323 "default",
324 )))
325 }
326 }
327
328 fn init_region(&self) -> RemoteResult<RegionProviderChain> {
330 Ok(
331 RegionProviderChain::first_try(self.region.as_ref().cloned().map(Region::new))
332 .or_default_provider()
333 .or_else(Region::new("us-west-2")),
334 )
335 }
336
337 fn make_client(
339 &self,
340 region: Option<Region>,
341 credentials: impl ProvideCredentials + 'static,
342 ) -> S3Client {
343 let mut builder = S3ClientBuilder::new()
344 .credentials_provider(credentials)
345 .behavior_version_latest()
346 .region(region);
347 builder.set_force_path_style(Some(self.new_path_style));
348
349 builder.set_endpoint_url(self.endpoint.clone());
350
351 S3Client::from_conf(builder.build())
352 }
353}
354
355impl RemoteFs for AwsS3Fs {
356 fn connect(&mut self) -> RemoteResult<Welcome> {
357 debug!("Loading credentials... (profile {:?})", self.profile);
359 let region_provider = self.init_region()?;
360 let region = self.runtime.block_on(region_provider.region());
361 let credentials = self.load_credentials(region_provider)?;
362 trace!(
364 "Parsing region: {}; endpoint: {}",
365 self.region.as_deref().unwrap_or("NULL"),
366 self.endpoint.as_deref().unwrap_or("NULL")
367 );
368 debug!(
369 "Credentials loaded! Connecting to bucket {}...",
370 self.bucket_name
371 );
372 self.client = Some(self.make_client(region, credentials));
373 info!("Connection successfully established to s3 bucket");
374 Ok(Welcome::default())
375 }
376
377 fn disconnect(&mut self) -> RemoteResult<()> {
378 info!("Disconnecting from S3 bucket...");
379 match self.client.take() {
380 Some(bucket) => {
381 drop(bucket);
382 Ok(())
383 }
384 None => Err(RemoteError::new(RemoteErrorType::NotConnected)),
385 }
386 }
387
388 fn is_connected(&mut self) -> bool {
389 self.client.is_some()
390 }
391
392 fn pwd(&mut self) -> RemoteResult<PathBuf> {
393 self.check_connection()?;
394 Ok(self.wrkdir.clone())
395 }
396
397 fn change_dir(&mut self, dir: &Path) -> RemoteResult<PathBuf> {
398 self.check_connection()?;
399 if dir == Path::new("/") {
401 self.wrkdir = dir.to_path_buf();
402 debug!("New working directory: {}", self.wrkdir.display());
403 return Ok(self.wrkdir.clone());
404 }
405 debug!("Entering directory {}...", dir.display());
407 let dir_p: PathBuf = self.resolve(dir);
408 let dir_s: String = Self::fmt_path(dir_p.as_path(), true);
409 debug!("Searching for key {} (path: {})...", dir_s, dir_p.display());
410 if self
412 .stat_object(PathBuf::from(dir_s.as_str()).as_path())
413 .is_ok()
414 {
415 self.wrkdir = path_utils::absolutize(Path::new("/"), dir_p.as_path());
416 debug!("New working directory: {}", self.wrkdir.display());
417 Ok(self.wrkdir.clone())
418 } else {
419 Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory))
420 }
421 }
422
423 fn list_dir(&mut self, path: &Path) -> RemoteResult<Vec<File>> {
424 self.check_connection()?;
425 self.list_objects(path, true)
426 .map(|x| x.into_iter().map(|x| x.into()).collect())
427 }
428
429 fn stat(&mut self, path: &Path) -> RemoteResult<File> {
430 self.check_connection()?;
431 let path = self.resolve(path);
432 if let Ok(obj) = self.stat_object(path.as_path()) {
433 return Ok(obj.into());
434 }
435 trace!("Failed to stat object as file; trying as a directory...");
437 let path = PathBuf::from(Self::fmt_path(path.as_path(), true));
438 self.stat_object(path.as_path()).map(|x| x.into())
439 }
440
441 fn setstat(&mut self, _path: &Path, _metadata: Metadata) -> RemoteResult<()> {
442 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
443 }
444
445 fn exists(&mut self, path: &Path) -> RemoteResult<bool> {
446 match self.stat(path) {
447 Ok(_) => Ok(true),
448 Err(RemoteError {
449 kind: RemoteErrorType::NoSuchFileOrDirectory,
450 ..
451 }) => Ok(false),
452 Err(err) => Err(err),
453 }
454 }
455
456 fn remove_file(&mut self, path: &Path) -> RemoteResult<()> {
457 self.check_connection()?;
458 let path = Self::fmt_path(self.resolve(path).as_path(), false);
459 debug!("Removing object '{}'", path);
460 let fut = self
461 .client
462 .as_ref()
463 .unwrap()
464 .delete_object()
465 .bucket(self.bucket_name.as_str())
466 .key(path.as_str())
467 .send();
468
469 self.runtime
470 .block_on(fut)
471 .map_err(|e| {
472 RemoteError::new_ex(
473 RemoteErrorType::ProtocolError,
474 format!("Could not remove file: {}", e),
475 )
476 })
477 .map(|_| ())
478 }
479
480 fn remove_dir(&mut self, path: &Path) -> RemoteResult<()> {
481 self.check_connection()?;
482 if !self.exists(path).ok().unwrap_or(false) {
483 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
484 }
485 println!("{}", self.resolve(path).as_path().display());
486 let path = Self::fmt_path(self.resolve(path).as_path(), true);
487 debug!("Removing object {}...", path);
488 let fut = self
489 .client
490 .as_ref()
491 .unwrap()
492 .delete_object()
493 .bucket(self.bucket_name.as_str())
494 .key(path.as_str())
495 .send();
496
497 self.runtime.block_on(fut).map(|_| ()).map_err(|e| {
498 RemoteError::new_ex(
499 RemoteErrorType::ProtocolError,
500 format!("Could not remove directory: {}", e),
501 )
502 })
503 }
504
505 fn remove_dir_all(&mut self, path: &Path) -> RemoteResult<()> {
506 debug!("Removing all content of {}", path.display());
507 if self.remove_dir(path).is_err() {
508 self.remove_file(path)
509 } else {
510 Ok(())
511 }
512 }
513
514 fn create_dir(&mut self, path: &Path, _mode: UnixPex) -> RemoteResult<()> {
515 self.check_connection()?;
516 let dir: String = Self::fmt_path(self.resolve(path).as_path(), true);
517 debug!("Making directory {}...", dir);
518 if self
520 .stat_object(PathBuf::from(dir.as_str()).as_path())
521 .is_ok()
522 {
523 error!("Directory {} already exists", dir);
524 return Err(RemoteError::new(RemoteErrorType::DirectoryAlreadyExists));
525 }
526 let fut = self
527 .client
528 .as_ref()
529 .unwrap()
530 .put_object()
531 .bucket(self.bucket_name.as_str())
532 .key(dir.as_str())
533 .send();
534
535 self.runtime.block_on(fut).map(|_| ()).map_err(|e| {
536 RemoteError::new_ex(
537 RemoteErrorType::FileCreateDenied,
538 format!("Could not make directory: {}", e),
539 )
540 })
541 }
542
543 fn symlink(&mut self, _path: &Path, _target: &Path) -> RemoteResult<()> {
544 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
545 }
546
547 fn copy(&mut self, _src: &Path, _dest: &Path) -> RemoteResult<()> {
548 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
549 }
550
551 fn mov(&mut self, _src: &Path, _dest: &Path) -> RemoteResult<()> {
552 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
553 }
554
555 fn exec(&mut self, _cmd: &str) -> RemoteResult<(u32, String)> {
556 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
557 }
558
559 fn append(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
560 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
561 }
562
563 fn create(&mut self, _path: &Path, _metadata: &Metadata) -> RemoteResult<WriteStream> {
564 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
565 }
566
567 fn open(&mut self, _path: &Path) -> RemoteResult<ReadStream> {
568 Err(RemoteError::new(RemoteErrorType::UnsupportedFeature))
569 }
570
571 fn create_file(
572 &mut self,
573 path: &Path,
574 _metadata: &Metadata,
575 mut reader: Box<dyn Read + Send>,
576 ) -> RemoteResult<u64> {
577 self.check_connection()?;
578 let src = self.resolve(path);
579 let key = Self::fmt_path(src.as_path(), false);
580
581 debug!("Query PUT for key '{}'", key);
583 let mut buf = vec![0; MIN_MULTIPART_UPLOAD_SIZE];
584 let mut offset = 0;
585 let mut part_number = 1;
586
587 let fut = self
589 .client
590 .as_ref()
591 .unwrap()
592 .create_multipart_upload()
593 .bucket(self.bucket_name.as_str())
594 .key(key.as_str())
595 .send();
596
597 let upload = self.runtime.block_on(fut).map_err(|e| {
598 error!("Could not init multipart upload: {e:?}",);
599 RemoteError::new_ex(
600 RemoteErrorType::ProtocolError,
601 format!("Could not init multipart upload: {}", e),
602 )
603 })?;
604 let upload_id = upload.upload_id().ok_or(RemoteError::new_ex(
605 RemoteErrorType::ProtocolError,
606 "no multipart id",
607 ))?;
608 debug!("starting multipart upload with upload id {upload_id}");
609
610 let mut upload_parts: Vec<aws_sdk_s3::types::CompletedPart> = Vec::new();
611
612 loop {
613 let buflen = reader.read(&mut buf).map_err(|e| {
614 RemoteError::new_ex(
615 RemoteErrorType::IoError,
616 format!("Could not read file: {}", e),
617 )
618 })?;
619 if buflen == 0 {
620 break;
621 }
622
623 let bytestream = ByteStream::new(SdkBody::from(&buf[..buflen]));
624 debug!("sending part {part_number} for {key} at offset {offset} for {buflen} bytes");
625
626 let buflen = buflen as i64;
627
628 let fut = self
629 .client
630 .as_ref()
631 .unwrap()
632 .upload_part()
633 .bucket(self.bucket_name.as_str())
634 .key(key.as_str())
635 .upload_id(upload_id)
636 .body(bytestream)
637 .part_number(part_number)
638 .send();
639
640 let upload_part_res = self.runtime.block_on(fut).map_err(|e| {
641 error!("Could not put file: {e:?}",);
642 RemoteError::new_ex(
643 RemoteErrorType::ProtocolError,
644 format!("Could not put file: {}", e),
645 )
646 })?;
647
648 upload_parts.push(
649 CompletedPart::builder()
650 .e_tag(upload_part_res.e_tag.unwrap_or_default())
651 .part_number(part_number)
652 .build(),
653 );
654
655 debug!("written {buflen} bytes at offset {offset} for {key}");
656
657 offset += buflen;
658 part_number += 1;
659 }
660
661 let completed_multipart_upload: CompletedMultipartUpload =
663 CompletedMultipartUpload::builder()
664 .set_parts(Some(upload_parts))
665 .build();
666
667 let fut = self
668 .client
669 .as_ref()
670 .unwrap()
671 .complete_multipart_upload()
672 .bucket(self.bucket_name.as_str())
673 .key(key.as_str())
674 .upload_id(upload_id)
675 .multipart_upload(completed_multipart_upload)
676 .send();
677
678 self.runtime.block_on(fut).map_err(|e| {
679 error!("Could not complete multipart upload: {e:?}",);
680 RemoteError::new_ex(
681 RemoteErrorType::ProtocolError,
682 format!("Could not complete multipart upload: {}", e),
683 )
684 })?;
685
686 Ok(offset as u64)
687 }
688
689 fn open_file(
690 &mut self,
691 src: &Path,
692 mut dest: Box<dyn std::io::Write + Send>,
693 ) -> RemoteResult<u64> {
694 self.check_connection()?;
695 if !self.exists(src).ok().unwrap_or(false) {
696 return Err(RemoteError::new(RemoteErrorType::NoSuchFileOrDirectory));
697 }
698 let src = self.resolve(src);
699 let key = Self::fmt_path(src.as_path(), false);
700 info!("Query GET for key '{}'", key);
701
702 let fut = self
703 .client
704 .as_ref()
705 .unwrap()
706 .get_object()
707 .bucket(self.bucket_name.as_str())
708 .key(key)
709 .send();
710
711 let mut data = self
712 .runtime
713 .block_on(fut)
714 .map_err(|e| {
715 RemoteError::new_ex(
716 RemoteErrorType::ProtocolError,
717 format!("Could not get file: {}", e),
718 )
719 })?
720 .body;
721
722 let mut n = 0;
723
724 while let Some(bytes) = self.runtime.block_on(data.try_next()).map_err(|e| {
725 RemoteError::new_ex(
726 RemoteErrorType::ProtocolError,
727 format!("Could not get file: {}", e),
728 )
729 })? {
730 dest.write_all(&bytes).map_err(|e| {
731 RemoteError::new_ex(
732 RemoteErrorType::IoError,
733 format!("Could not write file: {}", e),
734 )
735 })?;
736 n += bytes.len() as u64;
737 }
738
739 Ok(n)
740 }
741}
742
743#[cfg(test)]
744mod test {
745
746 #[cfg(feature = "with-s3-ci")]
747 use std::env;
748 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
749 use std::io::Cursor;
750 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
751 use std::time::SystemTime;
752
753 use pretty_assertions::assert_eq;
754
755 use super::*;
756 use crate::mock::container::Minio;
757
758 #[test]
759 fn should_init_s3() {
760 let s3 = AwsS3Fs::new("aws-s3-test", &Arc::new(Runtime::new().unwrap()));
761 assert_eq!(s3.wrkdir.as_path(), Path::new("/"));
762 assert_eq!(s3.bucket_name.as_str(), "aws-s3-test");
763 assert!(s3.region.is_none());
764 assert!(s3.endpoint.is_none());
765 assert_eq!(s3.is_anonymous(), true);
766 assert_eq!(s3.new_path_style, false);
767 assert!(s3.client.is_none());
768 assert!(s3.access_key.is_none());
769 assert!(s3.profile.is_none());
770 assert!(s3.secret_key.is_none());
771 assert!(s3.security_token.is_none());
772 assert!(s3.session_token.is_none());
773 assert!(s3.secret_key.is_none());
774 assert!(s3.client.is_none());
775 }
776
777 #[test]
778 fn should_init_s3_with_options() {
779 let s3 = AwsS3Fs::new("aws-s3-test", &Arc::new(Runtime::new().unwrap()))
780 .region("eu-central-1")
781 .access_key("AKIA0000")
782 .profile("default")
783 .secret_access_key("PASSWORD")
784 .security_token("secret")
785 .session_token("token")
786 .new_path_style(true)
787 .endpoint("omar");
788 assert_eq!(s3.bucket_name.as_str(), "aws-s3-test");
789 assert_eq!(s3.region.as_deref().unwrap(), "eu-central-1");
790 assert_eq!(s3.access_key.as_deref().unwrap(), "AKIA0000");
791 assert_eq!(s3.secret_key.as_deref().unwrap(), "PASSWORD");
792 assert_eq!(s3.security_token.as_deref().unwrap(), "secret");
793 assert_eq!(s3.session_token.as_deref().unwrap(), "token");
794 assert_eq!(s3.endpoint.as_deref().unwrap(), "omar");
795 assert_eq!(s3.is_anonymous(), false);
796 assert_eq!(s3.new_path_style, true);
797 }
798
799 #[test]
800 fn s3_is_direct_child() {
801 assert_eq!(AwsS3Fs::is_direct_child("pippo/", ""), true);
802 assert_eq!(AwsS3Fs::is_direct_child("pippo/sottocartella/", ""), false);
803 assert_eq!(
804 AwsS3Fs::is_direct_child("pippo/sottocartella/", "pippo/"),
805 true
806 );
807 assert_eq!(
808 AwsS3Fs::is_direct_child("pippo/sottocartella/", "pippo"), false
810 );
811 assert_eq!(
812 AwsS3Fs::is_direct_child("pippo/sottocartella/readme.md", "pippo/sottocartella/"),
813 true
814 );
815 assert_eq!(
816 AwsS3Fs::is_direct_child("pippo/sottocartella/readme.md", "pippo/sottocartella/"),
817 true
818 );
819 }
820
821 #[test]
822 fn s3_resolve() {
823 let mut s3 = AwsS3Fs::new("aws-s3-test", &Arc::new(Runtime::new().unwrap()));
824 s3.wrkdir = PathBuf::from("/tmp");
825 assert_eq!(
827 s3.resolve(Path::new("/tmp/sottocartella/")).as_path(),
828 Path::new("tmp/sottocartella")
829 );
830 assert_eq!(
832 s3.resolve(Path::new("subfolder/")).as_path(),
833 Path::new("tmp/subfolder")
834 );
835 }
836
837 #[test]
838 fn s3_fmt_path() {
839 assert_eq!(
840 AwsS3Fs::fmt_path(Path::new("/tmp/omar.txt"), false).as_str(),
841 "tmp/omar.txt"
842 );
843 assert_eq!(
844 AwsS3Fs::fmt_path(Path::new("omar.txt"), false).as_str(),
845 "omar.txt"
846 );
847 assert_eq!(
848 AwsS3Fs::fmt_path(Path::new("/tmp/subfolder"), true).as_str(),
849 "tmp/subfolder/"
850 );
851 assert_eq!(
852 AwsS3Fs::fmt_path(Path::new("tmp/subfolder"), true).as_str(),
853 "tmp/subfolder/"
854 );
855 assert_eq!(AwsS3Fs::fmt_path(Path::new("tmp"), true).as_str(), "tmp/");
856 assert_eq!(AwsS3Fs::fmt_path(Path::new("tmp/"), true).as_str(), "tmp/");
857 assert_eq!(AwsS3Fs::fmt_path(Path::new("/"), true).as_str(), "");
858 }
859
860 #[test]
861 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
862 fn should_not_append_to_file() {
863 crate::mock::logger();
864 let Ctx {
865 mut client,
866 container: _container,
867 } = setup_client();
868 let p = Path::new("a.txt");
870 let file_data = "Hello, world!\n";
872 let reader = Cursor::new(file_data.as_bytes());
873 assert!(
874 client
875 .append_file(p, &Metadata::default(), Box::new(reader))
876 .is_err()
877 );
878 finalize_client(client);
879 }
880
881 #[test]
882 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
883 fn should_change_directory() {
884 crate::mock::logger();
885 let Ctx {
886 mut client,
887 container: _container,
888 } = setup_client();
889 let pwd = client.pwd().ok().unwrap();
890 assert!(client.change_dir(Path::new("/")).is_ok());
891 assert!(client.change_dir(pwd.as_path()).is_ok());
892 finalize_client(client);
893 }
894
895 #[test]
896 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
897 fn should_not_change_directory() {
898 crate::mock::logger();
899 let Ctx {
900 mut client,
901 container: _container,
902 } = setup_client();
903 assert!(
904 client
905 .change_dir(Path::new("/tmp/sdfghjuireghiuergh/useghiyuwegh"))
906 .is_err()
907 );
908 finalize_client(client);
909 }
910
911 #[test]
912 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
913 fn should_not_copy_file() {
914 crate::mock::logger();
915 let Ctx {
916 mut client,
917 container: _container,
918 } = setup_client();
919 let p = Path::new("a.txt");
921 let file_data = "test data\n";
922 let reader = Cursor::new(file_data.as_bytes());
923 let mut metadata = Metadata::default();
924 metadata.size = file_data.len() as u64;
925 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
926 assert!(client.copy(p, Path::new("aaa/bbbb/ccc/b.txt")).is_err());
927 finalize_client(client);
928 }
929
930 #[test]
931 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
932 fn should_create_directory() {
933 crate::mock::logger();
934 let Ctx {
935 mut client,
936 container: _container,
937 } = setup_client();
938 assert!(
940 client
941 .create_dir(Path::new("mydir"), UnixPex::from(0o755))
942 .is_ok()
943 );
944 finalize_client(client);
945 }
946
947 #[test]
948 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
949 fn should_not_create_directory_cause_already_exists() {
950 crate::mock::logger();
951 let Ctx {
952 mut client,
953 container: _container,
954 } = setup_client();
955 assert!(
957 client
958 .create_dir(Path::new("mydir"), UnixPex::from(0o755))
959 .is_ok()
960 );
961 assert_eq!(
962 client
963 .create_dir(Path::new("mydir"), UnixPex::from(0o755))
964 .err()
965 .unwrap()
966 .kind,
967 RemoteErrorType::DirectoryAlreadyExists
968 );
969 finalize_client(client);
970 }
971
972 #[test]
973 #[cfg(feature = "with-s3-ci")]
974 fn should_not_create_directory() {
975 crate::mock::logger();
976 let Ctx {
977 mut client,
978 container: _container,
979 } = setup_client();
980 assert!(
982 client
983 .create_dir(
984 Path::new("/tmp/werfgjwerughjwurih/iwerjghiwgui"),
985 UnixPex::from(0o755)
986 )
987 .is_err()
988 );
989 finalize_client(client);
990 }
991
992 #[test]
993 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
994 fn should_create_file() {
995 crate::mock::logger();
996 let Ctx {
997 mut client,
998 container: _container,
999 } = setup_client();
1000 let p = Path::new("a.txt");
1002 let file_data = "test data\n";
1003 let reader = Cursor::new(file_data.as_bytes());
1004 let mut metadata = Metadata::default();
1005 metadata.size = file_data.len() as u64;
1006 assert_eq!(
1007 client
1008 .create_file(p, &metadata, Box::new(reader))
1009 .ok()
1010 .unwrap(),
1011 10
1012 );
1013 assert_eq!(client.stat(p).ok().unwrap().metadata().size, 10);
1015 finalize_client(client);
1016 }
1017
1018 #[test]
1019 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1020 fn should_not_exec_command() {
1021 crate::mock::logger();
1022 let Ctx {
1023 mut client,
1024 container: _container,
1025 } = setup_client();
1026 assert!(client.exec("echo 5").is_err());
1027 finalize_client(client);
1028 }
1029
1030 #[test]
1031 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1032 fn should_tell_whether_file_exists() {
1033 crate::mock::logger();
1034 let Ctx {
1035 mut client,
1036 container: _container,
1037 } = setup_client();
1038 let p = Path::new("a.txt");
1040 let file_data = "test data\n";
1041 let reader = Cursor::new(file_data.as_bytes());
1042 let mut metadata = Metadata::default();
1043 metadata.size = file_data.len() as u64;
1044 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1045 assert_eq!(client.exists(p).ok().unwrap(), true);
1047 assert_eq!(client.exists(Path::new("b.txt")).ok().unwrap(), false);
1048 assert_eq!(
1049 client.exists(Path::new("/tmp/ppppp/bhhrhu")).ok().unwrap(),
1050 false
1051 );
1052 finalize_client(client);
1053 }
1054
1055 #[test]
1056 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1057 fn should_list_dir() {
1058 crate::mock::logger();
1059 let Ctx {
1060 mut client,
1061 container: _container,
1062 } = setup_client();
1063 let wrkdir = client.pwd().ok().unwrap();
1065 let p = Path::new("a.txt");
1066 let file_data = "test data\n";
1067 let reader = Cursor::new(file_data.as_bytes());
1068 let mut metadata = Metadata::default();
1069 metadata.size = file_data.len() as u64;
1070 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1071 let file = client
1073 .list_dir(wrkdir.as_path())
1074 .ok()
1075 .unwrap()
1076 .get(0)
1077 .unwrap()
1078 .clone();
1079 assert_eq!(file.name().as_str(), "a.txt");
1080 let mut expected_path = wrkdir;
1081 expected_path.push(p);
1082 assert_eq!(file.path.as_path(), expected_path.as_path());
1083 assert_eq!(file.extension().as_deref().unwrap(), "txt");
1084 assert_eq!(file.metadata.size, 10);
1085 assert_eq!(file.metadata.mode, None);
1086 finalize_client(client);
1087 }
1088
1089 #[test]
1090 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1091 fn should_not_move_file() {
1092 crate::mock::logger();
1093 let Ctx {
1094 mut client,
1095 container: _container,
1096 } = setup_client();
1097 let p = Path::new("a.txt");
1099 let file_data = "test data\n";
1100 let reader = Cursor::new(file_data.as_bytes());
1101 let mut metadata = Metadata::default();
1102 metadata.size = file_data.len() as u64;
1103 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1104 let dest = Path::new("b.txt");
1105 assert!(client.mov(p, dest).is_err());
1106 finalize_client(client);
1107 }
1108
1109 #[test]
1110 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1111 fn should_open_file() {
1112 crate::mock::logger();
1113 let Ctx {
1114 mut client,
1115 container: _container,
1116 } = setup_client();
1117 let p = Path::new("a.txt");
1119 let file_data = "test data\n";
1120 let reader = Cursor::new(file_data.as_bytes());
1121 let mut metadata = Metadata::default();
1122 metadata.size = file_data.len() as u64;
1123 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1124 let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
1126 assert_eq!(client.open_file(p, buffer).ok().unwrap(), 10);
1127 finalize_client(client);
1128 }
1129
1130 #[test]
1131 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1132 fn should_write_big_data() {
1133 crate::mock::logger();
1134 let Ctx {
1135 mut client,
1136 container: _container,
1137 } = setup_client();
1138 let p = Path::new("a.txt");
1140 let file_data = vec![1; MIN_MULTIPART_UPLOAD_SIZE * 2];
1141 let reader = Cursor::new(file_data);
1142 let mut metadata = Metadata::default();
1143 metadata.size = (MIN_MULTIPART_UPLOAD_SIZE * 2) as u64;
1144 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1145 let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
1147 assert_eq!(
1148 client.open_file(p, buffer).ok().unwrap(),
1149 (MIN_MULTIPART_UPLOAD_SIZE * 2) as u64
1150 );
1151 finalize_client(client);
1152 }
1153
1154 #[test]
1155 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1156 fn should_not_open_file() {
1157 crate::mock::logger();
1158 let Ctx {
1159 mut client,
1160 container: _container,
1161 } = setup_client();
1162 let buffer: Box<dyn std::io::Write + Send> = Box::new(Vec::with_capacity(512));
1164 assert!(
1165 client
1166 .open_file(Path::new("/tmp/aashafb/hhh"), buffer)
1167 .is_err()
1168 );
1169 finalize_client(client);
1170 }
1171
1172 #[test]
1173 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1174 fn should_print_working_directory() {
1175 crate::mock::logger();
1176 let Ctx {
1177 mut client,
1178 container: _container,
1179 } = setup_client();
1180 assert!(client.pwd().is_ok());
1181 finalize_client(client);
1182 }
1183
1184 #[test]
1185 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1186 fn should_remove_dir_all() {
1187 crate::mock::logger();
1188 let Ctx {
1189 mut client,
1190 container: _container,
1191 } = setup_client();
1192 let mut dir_path = client.pwd().ok().unwrap();
1194 dir_path.push(Path::new("test/"));
1195 assert!(
1196 client
1197 .create_dir(dir_path.as_path(), UnixPex::from(0o775))
1198 .is_ok()
1199 );
1200 let mut file_path = dir_path.clone();
1202 file_path.push(Path::new("a.txt"));
1203 let file_data = "test data\n";
1204 let reader = Cursor::new(file_data.as_bytes());
1205 let mut metadata = Metadata::default();
1206 metadata.size = file_data.len() as u64;
1207 assert!(
1208 client
1209 .create_file(file_path.as_path(), &metadata, Box::new(reader))
1210 .is_ok()
1211 );
1212 assert!(client.remove_dir_all(dir_path.as_path()).is_ok());
1214 finalize_client(client);
1215 }
1216
1217 #[test]
1218 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1219 fn should_remove_dir() {
1220 crate::mock::logger();
1221 let Ctx {
1222 mut client,
1223 container: _container,
1224 } = setup_client();
1225 let mut dir_path = client.pwd().ok().unwrap();
1227 dir_path.push(Path::new("test/"));
1228 assert!(
1229 client
1230 .create_dir(dir_path.as_path(), UnixPex::from(0o775))
1231 .is_ok()
1232 );
1233 assert!(client.remove_dir(dir_path.as_path()).is_ok());
1234 finalize_client(client);
1235 }
1236
1237 #[test]
1238 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1239 fn should_not_remove_dir() {
1240 crate::mock::logger();
1241 let Ctx {
1242 mut client,
1243 container: _container,
1244 } = setup_client();
1245 assert!(client.remove_dir(Path::new("test/")).is_err());
1247 finalize_client(client);
1248 }
1249
1250 #[test]
1251 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1252 fn should_remove_file() {
1253 crate::mock::logger();
1254 let Ctx {
1255 mut client,
1256 container: _container,
1257 } = setup_client();
1258 let p = Path::new("a.txt");
1260 let file_data = "test data\n";
1261 let reader = Cursor::new(file_data.as_bytes());
1262 let mut metadata = Metadata::default();
1263 metadata.size = file_data.len() as u64;
1264 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1265 assert!(client.remove_file(p).is_ok());
1266 assert!(client.stat(p).is_err());
1268 finalize_client(client);
1269 }
1270
1271 #[test]
1272 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1273 fn should_not_setstat_file() {
1274 crate::mock::logger();
1275 let Ctx {
1276 mut client,
1277 container: _container,
1278 } = setup_client();
1279 let p = Path::new("a.sh");
1281 let file_data = "echo 5\n";
1282 let reader = Cursor::new(file_data.as_bytes());
1283 let mut metadata = Metadata::default();
1284 metadata.size = file_data.len() as u64;
1285 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1286 assert!(
1287 client
1288 .setstat(
1289 p,
1290 Metadata {
1291 accessed: Some(SystemTime::UNIX_EPOCH),
1292 created: Some(SystemTime::UNIX_EPOCH),
1293 gid: Some(1000),
1294 file_type: remotefs::fs::FileType::File,
1295 mode: Some(UnixPex::from(0o755)),
1296 modified: Some(SystemTime::UNIX_EPOCH),
1297 size: 7,
1298 symlink: None,
1299 uid: Some(1000),
1300 }
1301 )
1302 .is_err()
1303 );
1304 finalize_client(client);
1305 }
1306
1307 #[test]
1308 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1309 fn should_stat_file() {
1310 crate::mock::logger();
1311 let Ctx {
1312 mut client,
1313 container: _container,
1314 } = setup_client();
1315 let p = Path::new("a.sh");
1317 let file_data = "echo 5\n";
1318 let reader = Cursor::new(file_data.as_bytes());
1319 let mut metadata = Metadata::default();
1320 metadata.size = file_data.len() as u64;
1321 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1322 let entry = client.stat(p).ok().unwrap();
1323 assert_eq!(entry.name(), "a.sh");
1324 let mut expected_path = client.pwd().ok().unwrap();
1325 expected_path.push("a.sh");
1326 assert_eq!(entry.path(), expected_path.as_path());
1327 let meta = entry.metadata();
1328 assert_eq!(meta.size, 7);
1329 finalize_client(client);
1330 }
1331
1332 #[test]
1333 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1334 fn should_not_stat_file() {
1335 crate::mock::logger();
1336 let Ctx {
1337 mut client,
1338 container: _container,
1339 } = setup_client();
1340 let p = Path::new("a.sh");
1342 assert!(client.stat(p).is_err());
1343 finalize_client(client);
1344 }
1345
1346 #[test]
1347 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1348 fn should_make_symlink() {
1349 crate::mock::logger();
1350 let Ctx {
1351 mut client,
1352 container: _container,
1353 } = setup_client();
1354 let p = Path::new("a.sh");
1356 let file_data = "echo 5\n";
1357 let reader = Cursor::new(file_data.as_bytes());
1358 let mut metadata = Metadata::default();
1359 metadata.size = file_data.len() as u64;
1360 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1361 let symlink = Path::new("b.sh");
1362 assert!(client.symlink(symlink, p).is_err());
1363 finalize_client(client);
1364 }
1365
1366 #[test]
1367 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1368 fn should_not_make_symlink() {
1369 crate::mock::logger();
1370 let Ctx {
1371 mut client,
1372 container: _container,
1373 } = setup_client();
1374 let p = Path::new("a.sh");
1376 let file_data = "echo 5\n";
1377 let reader = Cursor::new(file_data.as_bytes());
1378 let mut metadata = Metadata::default();
1379 metadata.size = file_data.len() as u64;
1380 assert!(client.create_file(p, &metadata, Box::new(reader)).is_ok());
1381 let symlink = Path::new("b.sh");
1382 let file_data = "echo 5\n";
1383 let reader = Cursor::new(file_data.as_bytes());
1384 assert!(
1385 client
1386 .create_file(symlink, &metadata, Box::new(reader))
1387 .is_ok()
1388 );
1389 assert!(client.symlink(symlink, p).is_err());
1390 assert!(client.remove_file(symlink).is_ok());
1391 assert!(client.symlink(symlink, Path::new("c.sh")).is_err());
1392 finalize_client(client);
1393 }
1394
1395 #[test]
1396 fn should_return_errors_on_uninitialized_client() {
1397 let mut client =
1398 AwsS3Fs::new("aws-s3-test", &Arc::new(Runtime::new().unwrap())).region("eu-central-1");
1399 assert!(client.change_dir(Path::new("/tmp")).is_err());
1400 assert!(
1401 client
1402 .copy(Path::new("/nowhere"), PathBuf::from("/culonia").as_path())
1403 .is_err()
1404 );
1405 assert!(client.exec("echo 5").is_err());
1406 assert!(client.disconnect().is_err());
1407 assert!(client.symlink(Path::new("/a"), Path::new("/b")).is_err());
1408 assert!(client.list_dir(Path::new("/tmp")).is_err());
1409 assert!(
1410 client
1411 .create_dir(Path::new("/tmp"), UnixPex::from(0o755))
1412 .is_err()
1413 );
1414 assert!(client.pwd().is_err());
1415 assert!(client.remove_dir_all(Path::new("/nowhere")).is_err());
1416 assert!(
1417 client
1418 .mov(Path::new("/nowhere"), Path::new("/culonia"))
1419 .is_err()
1420 );
1421 assert!(client.stat(Path::new("/tmp")).is_err());
1422 assert!(
1423 client
1424 .setstat(Path::new("/tmp"), Metadata::default())
1425 .is_err()
1426 );
1427 assert!(client.open(Path::new("/tmp/pippo.txt")).is_err());
1428 assert!(
1429 client
1430 .create(Path::new("/tmp/pippo.txt"), &Metadata::default())
1431 .is_err()
1432 );
1433 assert!(
1434 client
1435 .append(Path::new("/tmp/pippo.txt"), &Metadata::default())
1436 .is_err()
1437 );
1438 }
1439
1440 fn is_send<T: Send>(_send: T) {}
1441
1442 fn is_sync<T: Sync>(_sync: T) {}
1443
1444 #[test]
1445 fn test_should_be_sync() {
1446 let client = AwsS3Fs::new("bucket", &Arc::new(Runtime::new().unwrap()));
1447
1448 is_sync(client);
1449 }
1450
1451 #[test]
1452 fn test_should_be_send() {
1453 let client = AwsS3Fs::new("bucket", &Arc::new(Runtime::new().unwrap()));
1454
1455 is_send(client);
1456 }
1457
1458 #[allow(dead_code)]
1461 struct Ctx {
1462 client: AwsS3Fs,
1463 #[cfg(feature = "with-containers")]
1464 container: Minio,
1465 #[cfg(all(feature = "with-s3-ci", not(feature = "with-containers")))]
1466 container: (),
1467 }
1468
1469 #[cfg(all(feature = "with-s3-ci", not(feature = "with-containers")))]
1470 fn setup_client() -> Ctx {
1471 let bucket = env!("AWS_S3_BUCKET");
1473 let mut client = AwsS3Fs::new(bucket, &Arc::new(Runtime::new().unwrap()));
1474 assert!(client.connect().is_ok());
1475 let tempdir = PathBuf::from(generate_tempdir());
1477 assert!(
1478 client
1479 .create_dir(tempdir.as_path(), UnixPex::from(0o775))
1480 .is_ok()
1481 );
1482 let err = client.change_dir(tempdir.as_path());
1484 if err.is_err() {
1485 println!("Error: {:?}", err);
1486 }
1487 assert!(client.change_dir(tempdir.as_path()).is_ok());
1488 Ctx {
1489 client,
1490 container: (),
1491 }
1492 }
1493
1494 #[cfg(feature = "with-containers")]
1495 fn setup_client() -> Ctx {
1496 let minio = Minio::start();
1497 let port = minio.port();
1498
1499 let runtime = Arc::new(Runtime::new().expect("Could not create runtime"));
1501 let mut client = AwsS3Fs::new("github-ci", &runtime)
1502 .endpoint(format!("http://localhost:{port}"))
1503 .access_key("minioadmin")
1504 .secret_access_key("minioadmin")
1505 .new_path_style(true);
1506
1507 assert!(client.connect().is_ok());
1509
1510 let fut = client
1512 .client()
1513 .unwrap()
1514 .create_bucket()
1515 .bucket("github-ci")
1516 .send();
1517 let res = runtime.block_on(fut);
1518
1519 assert!(res.is_ok());
1520
1521 let tempdir = PathBuf::from(generate_tempdir());
1523 assert!(
1524 client
1525 .create_dir(tempdir.as_path(), UnixPex::from(0o775))
1526 .is_ok()
1527 );
1528 assert!(client.change_dir(tempdir.as_path()).is_ok());
1530 Ctx {
1531 client,
1532 container: minio,
1533 }
1534 }
1535
1536 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1537 fn finalize_client(mut client: AwsS3Fs) {
1538 let wrkdir = client.pwd().ok().unwrap();
1540 assert!(client.remove_dir_all(wrkdir.as_path()).is_ok());
1542 assert!(client.disconnect().is_ok());
1543 }
1544
1545 #[cfg(any(feature = "with-s3-ci", feature = "with-containers"))]
1546 fn generate_tempdir() -> String {
1547 use rand::distr::Alphanumeric;
1548 use rand::{Rng, rng};
1549 let mut rng = rng();
1550 let name: String = std::iter::repeat(())
1551 .map(|()| rng.sample(Alphanumeric))
1552 .map(char::from)
1553 .take(8)
1554 .collect();
1555 format!("/github-ci/temp_{}/", name)
1556 }
1557}