1use std::collections::{HashMap, VecDeque};
2use std::sync::{Arc, OnceLock};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use futures::stream::BoxStream;
6use futures::{StreamExt, stream};
7use tokio::runtime::{Handle, Runtime};
8use url::Url;
9
10use crate::acl::{AclEntry, AclStatus};
11use crate::common::config::{self, Configuration};
12use crate::ec::resolve_ec_policy;
13use crate::error::{HdfsError, Result};
14use crate::file::{FileReader, FileWriter};
15use crate::hdfs::crypto::FileCryptoCodec;
16use crate::hdfs::protocol::NamenodeProtocol;
17use crate::hdfs::proxy::NameServiceProxy;
18use crate::proto::hdfs::hdfs_file_status_proto::FileType;
19#[cfg(feature = "kms")]
20use crate::security::kms::KmsClient;
21use crate::security::user::User;
22
23use crate::glob::{GlobPattern, expand_glob, get_path_components, unescape_component};
24use crate::proto::hdfs::{ContentSummaryProto, FileEncryptionInfoProto, HdfsFileStatusProto};
25
26const TRASH_ROOT_DIR: &str = ".Trash";
27const TRASH_CURRENT_DIR: &str = "Current";
28const TRASH_DIR_PERMISSION: u32 = 0o700;
29
30#[derive(Clone)]
31pub struct WriteOptions {
32 pub block_size: Option<u64>,
34 pub replication: Option<u32>,
36 pub permission: u32,
39 pub overwrite: bool,
42 pub create_parent: bool,
45}
46
47impl Default for WriteOptions {
48 fn default() -> Self {
49 Self {
50 block_size: None,
51 replication: None,
52 permission: 0o644,
53 overwrite: false,
54 create_parent: true,
55 }
56 }
57}
58
59impl AsRef<WriteOptions> for WriteOptions {
60 fn as_ref(&self) -> &WriteOptions {
61 self
62 }
63}
64
65impl WriteOptions {
66 pub fn block_size(mut self, block_size: u64) -> Self {
68 self.block_size = Some(block_size);
69 self
70 }
71
72 pub fn replication(mut self, replication: u32) -> Self {
74 self.replication = Some(replication);
75 self
76 }
77
78 pub fn permission(mut self, permission: u32) -> Self {
80 self.permission = permission;
81 self
82 }
83
84 pub fn overwrite(mut self, overwrite: bool) -> Self {
86 self.overwrite = overwrite;
87 self
88 }
89
90 pub fn create_parent(mut self, create_parent: bool) -> Self {
92 self.create_parent = create_parent;
93 self
94 }
95}
96
97#[derive(Debug, Clone)]
98struct MountLink {
99 viewfs_path: String,
100 hdfs_path: String,
101 protocol: Arc<NamenodeProtocol>,
102}
103
104impl MountLink {
105 fn new(viewfs_path: &str, hdfs_path: &str, protocol: Arc<NamenodeProtocol>) -> Self {
106 Self {
108 viewfs_path: viewfs_path.trim_end_matches("/").to_string(),
109 hdfs_path: hdfs_path.trim_end_matches("/").to_string(),
110 protocol,
111 }
112 }
113 fn resolve(&self, path: &str) -> Option<String> {
115 if path == self.viewfs_path {
118 Some(self.hdfs_path.clone())
119 } else {
120 path.strip_prefix(&format!("{}/", self.viewfs_path))
121 .map(|relative_path| format!("{}/{}", self.hdfs_path, relative_path))
122 }
123 }
124}
125
126#[derive(Debug)]
127struct MountTable {
128 mounts: Vec<MountLink>,
129 fallback: MountLink,
130 home_dir: String,
131}
132
133impl MountTable {
134 fn resolve(&self, src: &str) -> (&MountLink, String) {
135 let path = if src.starts_with('/') {
136 src.to_string()
137 } else {
138 format!("{}/{}", self.home_dir, src)
139 };
140
141 for link in self.mounts.iter() {
142 if let Some(resolved) = link.resolve(&path) {
143 return (link, resolved);
144 }
145 }
146 (&self.fallback, self.fallback.resolve(&path).unwrap())
147 }
148}
149
150fn build_home_dir(
151 scheme: &str,
152 host: Option<&str>,
153 config: &Configuration,
154 username: &str,
155) -> String {
156 let prefix = match scheme {
157 "hdfs" => config.get("dfs.user.home.dir.prefix"),
158 "viewfs" => {
159 host.and_then(|host| config.get(&format!("fs.viewfs.mounttable.{host}.homedir")))
160 }
161 _ => None,
162 }
163 .unwrap_or("/user");
164
165 let prefix = prefix.trim_end_matches('/');
166 if prefix.is_empty() {
167 format!("/{username}")
168 } else {
169 format!("{prefix}/{username}")
170 }
171}
172
173#[derive(Debug)]
175pub enum IORuntime {
176 Runtime(Runtime),
177 Handle(Handle),
178}
179
180impl From<Runtime> for IORuntime {
181 fn from(value: Runtime) -> Self {
182 Self::Runtime(value)
183 }
184}
185
186impl From<Handle> for IORuntime {
187 fn from(value: Handle) -> Self {
188 Self::Handle(value)
189 }
190}
191
192impl IORuntime {
193 fn handle(&self) -> Handle {
194 match self {
195 Self::Runtime(runtime) => runtime.handle().clone(),
196 Self::Handle(handle) => handle.clone(),
197 }
198 }
199}
200
201#[derive(Default)]
265pub struct ClientBuilder {
266 url: Option<String>,
267 config: Option<HashMap<String, String>>,
268 config_dir: Option<String>,
269 runtime: Option<IORuntime>,
270 user: Option<String>,
271}
272
273impl ClientBuilder {
274 pub fn new() -> Self {
276 Self::default()
277 }
278
279 pub fn with_url(mut self, url: impl Into<String>) -> Self {
281 self.url = Some(url.into());
282 self
283 }
284
285 pub fn with_config(
287 mut self,
288 config: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
289 ) -> Self {
290 self.config = Some(
291 config
292 .into_iter()
293 .map(|(k, v)| (k.into(), v.into()))
294 .collect(),
295 );
296 self
297 }
298
299 pub fn with_config_dir(mut self, config_dir: impl Into<String>) -> Self {
301 self.config_dir = Some(config_dir.into());
302 self
303 }
304
305 pub fn with_io_runtime(mut self, runtime: impl Into<IORuntime>) -> Self {
308 self.runtime = Some(runtime.into());
309 self
310 }
311
312 pub fn with_user(mut self, user: impl Into<String>) -> Self {
314 self.user = Some(user.into());
315 self
316 }
317
318 pub fn build(self) -> Result<Client> {
320 let config = Configuration::new(self.config_dir, self.config)?;
321 let url = if let Some(url) = self.url {
322 Url::parse(&url)?
323 } else {
324 Client::default_fs(&config)?
325 };
326
327 Client::build(&url, config, self.runtime, self.user)
328 }
329}
330
331#[derive(Clone, Debug)]
332enum RuntimeHolder {
333 Custom(Arc<IORuntime>),
334 Default(Arc<OnceLock<Runtime>>),
335}
336
337impl RuntimeHolder {
338 fn new(rt: Option<IORuntime>) -> Self {
339 if let Some(rt) = rt {
340 Self::Custom(Arc::new(rt))
341 } else {
342 Self::Default(Arc::new(OnceLock::new()))
343 }
344 }
345
346 fn get_handle(&self) -> Handle {
347 match self {
348 Self::Custom(rt) => rt.handle().clone(),
349 Self::Default(rt) => match Handle::try_current() {
350 Ok(handle) => handle,
351 Err(_) => rt
352 .get_or_init(|| Runtime::new().expect("Failed to create tokio runtime"))
353 .handle()
354 .clone(),
355 },
356 }
357 }
358}
359
360#[derive(Clone, Debug)]
362pub struct Client {
363 mount_table: Arc<MountTable>,
364 config: Arc<Configuration>,
365 rt_holder: RuntimeHolder,
368 #[cfg(feature = "kms")]
371 kms_client: Option<Arc<KmsClient>>,
372}
373
374impl Client {
375 fn default_fs(config: &Configuration) -> Result<Url> {
376 let url = config
377 .get(config::DEFAULT_FS)
378 .ok_or(HdfsError::InvalidArgument(format!(
379 "No {} setting found",
380 config::DEFAULT_FS
381 )))?;
382 Ok(Url::parse(url)?)
383 }
384
385 fn build(
386 url: &Url,
387 config: Configuration,
388 rt: Option<IORuntime>,
389 user: Option<String>,
390 ) -> Result<Self> {
391 let resolved_url = if !url.has_host() {
392 let default_url = Self::default_fs(&config)?;
393 if url.scheme() != default_url.scheme() || !default_url.has_host() {
394 return Err(HdfsError::InvalidArgument(
395 "URL must contain a host".to_string(),
396 ));
397 }
398 default_url
399 } else {
400 url.clone()
401 };
402
403 let config = Arc::new(config);
404
405 let rt_holder = RuntimeHolder::new(rt);
406
407 let user_info = User::get_user_info(user.clone(), config.security_enabled());
408 let username = user_info
409 .effective_user
410 .as_deref()
411 .or(user_info.real_user.as_deref())
412 .expect("User info must include a username");
413 let home_dir = build_home_dir(
414 resolved_url.scheme(),
415 resolved_url.host_str(),
416 config.as_ref(),
417 username,
418 );
419
420 let mount_table = match url.scheme() {
421 "hdfs" => {
422 let proxy = NameServiceProxy::new(
423 &resolved_url,
424 Arc::clone(&config),
425 rt_holder.get_handle(),
426 user.clone(),
427 )?;
428 let protocol = Arc::new(NamenodeProtocol::new(proxy, rt_holder.get_handle()));
429
430 MountTable {
431 mounts: Vec::new(),
432 fallback: MountLink::new("/", "/", protocol),
433 home_dir,
434 }
435 }
436 "viewfs" => Self::build_mount_table(
437 resolved_url.host_str().expect("URL must have a host"),
439 Arc::clone(&config),
440 rt_holder.get_handle(),
441 user.clone(),
442 home_dir,
443 )?,
444 _ => {
445 return Err(HdfsError::InvalidArgument(
446 "Only `hdfs` and `viewfs` schemes are supported".to_string(),
447 ));
448 }
449 };
450
451 #[cfg(feature = "kms")]
452 let kms_client = KmsClient::from_config(config.as_ref(), None, username.to_string())?;
453
454 Ok(Self {
455 mount_table: Arc::new(mount_table),
456 config,
457 rt_holder,
458 #[cfg(feature = "kms")]
459 kms_client,
460 })
461 }
462
463 fn build_mount_table(
464 host: &str,
465 config: Arc<Configuration>,
466 handle: Handle,
467 effective_user: Option<String>,
468 home_dir: String,
469 ) -> Result<MountTable> {
470 let mut mounts: Vec<MountLink> = Vec::new();
471 let mut fallback: Option<MountLink> = None;
472
473 for (viewfs_path, hdfs_url) in config.get_mount_table(host).iter() {
474 let url = Url::parse(hdfs_url)?;
475 if !url.has_host() {
476 return Err(HdfsError::InvalidArgument(
477 "URL must contain a host".to_string(),
478 ));
479 }
480 if url.scheme() != "hdfs" {
481 return Err(HdfsError::InvalidArgument(
482 "Only hdfs mounts are supported for viewfs".to_string(),
483 ));
484 }
485 let proxy = NameServiceProxy::new(
486 &url,
487 Arc::clone(&config),
488 handle.clone(),
489 effective_user.clone(),
490 )?;
491 let protocol = Arc::new(NamenodeProtocol::new(proxy, handle.clone()));
492
493 if let Some(prefix) = viewfs_path {
494 mounts.push(MountLink::new(prefix, url.path(), protocol));
495 } else {
496 if fallback.is_some() {
497 return Err(HdfsError::InvalidArgument(
498 "Multiple viewfs fallback links found".to_string(),
499 ));
500 }
501 fallback = Some(MountLink::new("/", url.path(), protocol));
502 }
503 }
504
505 if let Some(fallback) = fallback {
506 mounts.sort_by_key(|m| m.viewfs_path.chars().filter(|c| *c == '/').count());
508 mounts.reverse();
509
510 Ok(MountTable {
511 mounts,
512 fallback,
513 home_dir,
514 })
515 } else {
516 Err(HdfsError::InvalidArgument(
517 "No viewfs fallback mount found".to_string(),
518 ))
519 }
520 }
521
522 fn normalize_path(path: &str) -> String {
523 let mut normalized = if path.is_empty() {
524 "/".to_string()
525 } else {
526 path.to_string()
527 };
528 if !normalized.starts_with('/') {
529 normalized.insert(0, '/');
530 }
531 while normalized.len() > 1 && normalized.ends_with('/') {
532 normalized.pop();
533 }
534 normalized
535 }
536
537 fn join_paths(base: &str, suffix: &str) -> String {
538 if suffix.is_empty() {
539 return base.to_string();
540 }
541 let trimmed_base = if base.is_empty() { "/" } else { base };
542 let suffix = suffix.trim_start_matches('/');
543 if trimmed_base == "/" {
544 format!("/{suffix}")
545 } else {
546 format!("{}/{}", trimmed_base.trim_end_matches('/'), suffix)
547 }
548 }
549
550 fn is_prefix_path(parent: &str, child: &str) -> bool {
551 if parent == "/" {
552 return true;
553 }
554 child == parent || child.starts_with(&format!("{parent}/"))
555 }
556
557 fn current_time_millis() -> u64 {
558 SystemTime::now()
559 .duration_since(UNIX_EPOCH)
560 .unwrap_or_default()
561 .as_millis() as u64
562 }
563
564 fn absolute_path(&self, path: &str) -> String {
565 if path.starts_with('/') {
566 Self::normalize_path(path)
567 } else {
568 let home = self.mount_table.home_dir.trim_end_matches('/');
569 Self::normalize_path(&format!("{home}/{path}"))
570 }
571 }
572
573 fn trash_root_path(&self) -> String {
574 let home = Self::normalize_path(&self.mount_table.home_dir);
575 Self::join_paths(&home, TRASH_ROOT_DIR)
576 }
577
578 async fn trash_enabled(&self, path: &str) -> Result<bool> {
579 let (link, _) = self.mount_table.resolve(path);
580 let server_defaults = link.protocol.get_cached_server_defaults().await?;
581 Ok(server_defaults.trash_interval.unwrap_or_default() > 0)
582 }
583
584 fn split_parent_name(path: &str) -> Result<(String, String)> {
585 let normalized = Self::normalize_path(path);
586 if normalized == "/" {
587 return Err(HdfsError::InvalidArgument(
588 "Cannot move the root directory to trash".to_string(),
589 ));
590 }
591 let (parent, name) = normalized
592 .rsplit_once('/')
593 .expect("Normalized path always contains '/'");
594 let parent = if parent.is_empty() {
595 "/".to_string()
596 } else {
597 parent.to_string()
598 };
599 Ok((parent, name.to_string()))
600 }
601
602 async fn non_dir_ancestor(&self, path: &str) -> Result<Option<String>> {
603 let normalized = Self::normalize_path(path);
604 let mut current = "/".to_string();
605 for component in normalized.trim_start_matches('/').split('/') {
606 if component.is_empty() {
607 continue;
608 }
609 current = Self::join_paths(¤t, component);
610 match self.get_file_info(¤t).await {
611 Ok(status) => {
612 if !status.isdir {
613 return Ok(Some(current));
614 }
615 }
616 Err(HdfsError::FileNotFound(_)) => return Ok(None),
617 Err(err) => return Err(err),
618 }
619 }
620 Ok(None)
621 }
622
623 async fn ensure_unique_trash_path(&self, path: String) -> Result<String> {
624 let base = path.clone();
625 let mut candidate = path;
626 loop {
627 match self.get_file_info(&candidate).await {
628 Ok(_) => {
629 candidate = format!("{}{}", base, Self::current_time_millis());
630 }
631 Err(HdfsError::FileNotFound(_)) => return Ok(candidate),
632 Err(err) => return Err(err),
633 }
634 }
635 }
636
637 pub async fn get_file_info(&self, path: &str) -> Result<FileStatus> {
639 let (link, resolved_path) = self.mount_table.resolve(path);
640 match link.protocol.get_file_info(&resolved_path).await?.fs {
641 Some(status) => Ok(FileStatus::from(status, path)),
642 None => Err(HdfsError::FileNotFound(path.to_string())),
643 }
644 }
645
646 pub async fn list_status(&self, path: &str, recursive: bool) -> Result<Vec<FileStatus>> {
649 let iter = self.list_status_iter(path, recursive);
650 let statuses = iter
651 .into_stream()
652 .collect::<Vec<Result<FileStatus>>>()
653 .await;
654
655 let mut resolved_statues = Vec::<FileStatus>::with_capacity(statuses.len());
656 for status in statuses.into_iter() {
657 resolved_statues.push(status?);
658 }
659
660 Ok(resolved_statues)
661 }
662
663 pub fn list_status_iter(&self, path: &str, recursive: bool) -> ListStatusIterator {
665 ListStatusIterator::new(path.to_string(), Arc::clone(&self.mount_table), recursive)
666 }
667
668 pub async fn read(&self, path: &str) -> Result<FileReader> {
670 let (link, resolved_path) = self.mount_table.resolve(path);
671 let located_info = link
673 .protocol
674 .get_block_locations(&resolved_path, 0, i64::MAX as u64)
675 .await?;
676
677 if let Some(locations) = located_info.locations {
678 let ec_schema = if let Some(ec_policy) = locations.ec_policy.as_ref() {
679 Some(resolve_ec_policy(ec_policy)?)
680 } else {
681 None
682 };
683
684 let crypto = self
685 .build_crypto_codec(locations.file_encryption_info.as_ref())
686 .await?;
687
688 Ok(FileReader::new(
689 Arc::clone(&link.protocol),
690 locations,
691 ec_schema,
692 Arc::clone(&self.config),
693 self.rt_holder.get_handle(),
694 crypto,
695 ))
696 } else {
697 Err(HdfsError::FileNotFound(path.to_string()))
698 }
699 }
700
701 async fn build_crypto_codec(
705 &self,
706 info: Option<&FileEncryptionInfoProto>,
707 ) -> Result<Option<Arc<FileCryptoCodec>>> {
708 let Some(info) = info else {
709 return Ok(None);
710 };
711 #[cfg(feature = "kms")]
712 {
713 let kms = self.kms_client.as_ref().ok_or_else(|| {
714 HdfsError::OperationFailed(
715 "File is in an HDFS encryption zone but no KMS provider is configured \
716 (set `hadoop.security.key.provider.path` in core-site.xml)"
717 .to_string(),
718 )
719 })?;
720 let dek = kms.decrypt_edek(info).await?;
721 Ok(Some(Arc::new(FileCryptoCodec::new(info, dek)?)))
722 }
723 #[cfg(not(feature = "kms"))]
724 {
725 let _ = info;
726 Err(HdfsError::UnsupportedFeature(
727 "file is in an HDFS encryption zone; reading or writing it requires \
728 building hdfs-native with the `kms` cargo feature"
729 .to_string(),
730 ))
731 }
732 }
733
734 pub async fn create(
737 &self,
738 src: &str,
739 write_options: impl AsRef<WriteOptions>,
740 ) -> Result<FileWriter> {
741 let write_options = write_options.as_ref();
742
743 let (link, resolved_path) = self.mount_table.resolve(src);
744
745 let create_response = link
746 .protocol
747 .create(
748 &resolved_path,
749 write_options.permission,
750 write_options.overwrite,
751 write_options.create_parent,
752 write_options.replication,
753 write_options.block_size,
754 )
755 .await?;
756
757 match create_response.fs {
758 Some(status) => {
759 let crypto = match self
760 .build_crypto_codec(status.file_encryption_info.as_ref())
761 .await
762 {
763 Ok(c) => c,
764 Err(e) => {
765 let _ = self.delete(src, false).await;
769 return Err(e);
770 }
771 };
772
773 Ok(FileWriter::new(
774 Arc::clone(&link.protocol),
775 resolved_path,
776 status,
777 Arc::clone(&self.config),
778 self.rt_holder.get_handle(),
779 None,
780 crypto,
781 ))
782 }
783 None => Err(HdfsError::FileNotFound(src.to_string())),
784 }
785 }
786
787 fn needs_new_block(class: &str, msg: &str) -> bool {
788 class == "java.lang.UnsupportedOperationException" && msg.contains("NEW_BLOCK")
789 }
790
791 pub async fn append(&self, src: &str) -> Result<FileWriter> {
795 let (link, resolved_path) = self.mount_table.resolve(src);
796
797 let append_response = match link.protocol.append(&resolved_path, false).await {
800 Err(HdfsError::RPCError(class, msg)) if Self::needs_new_block(&class, &msg) => {
801 link.protocol.append(&resolved_path, true).await?
802 }
803 resp => resp?,
804 };
805
806 match append_response.stat {
807 Some(status) => {
808 let crypto = match self
809 .build_crypto_codec(status.file_encryption_info.as_ref())
810 .await
811 {
812 Ok(c) => c,
813 Err(e) => {
814 let _ = link
817 .protocol
818 .complete(
819 src,
820 append_response.block.as_ref().map(|b| b.b.clone()),
821 status.file_id,
822 )
823 .await;
824 return Err(e);
825 }
826 };
827
828 Ok(FileWriter::new(
829 Arc::clone(&link.protocol),
830 resolved_path,
831 status,
832 Arc::clone(&self.config),
833 self.rt_holder.get_handle(),
834 append_response.block,
835 crypto,
836 ))
837 }
838 None => Err(HdfsError::FileNotFound(src.to_string())),
839 }
840 }
841
842 pub async fn mkdirs(&self, path: &str, permission: u32, create_parent: bool) -> Result<()> {
850 let (link, resolved_path) = self.mount_table.resolve(path);
851 link.protocol
852 .mkdirs(&resolved_path, permission, create_parent)
853 .await
854 .map(|_| ())
855 }
856
857 async fn rename_internal(
858 &self,
859 src: &str,
860 dst: &str,
861 overwrite: bool,
862 move_to_trash: bool,
863 ) -> Result<()> {
864 let (src_link, src_resolved_path) = self.mount_table.resolve(src);
865 let (dst_link, dst_resolved_path) = self.mount_table.resolve(dst);
866 if src_link.viewfs_path == dst_link.viewfs_path {
867 src_link
868 .protocol
869 .rename(
870 &src_resolved_path,
871 &dst_resolved_path,
872 overwrite,
873 move_to_trash,
874 )
875 .await
876 .map(|_| ())
877 } else {
878 Err(HdfsError::InvalidArgument(
879 "Cannot rename across different name services".to_string(),
880 ))
881 }
882 }
883
884 pub async fn rename(&self, src: &str, dst: &str, overwrite: bool) -> Result<()> {
886 self.rename_internal(src, dst, overwrite, false).await
887 }
888
889 pub async fn delete(&self, path: &str, recursive: bool) -> Result<bool> {
892 let (link, resolved_path) = self.mount_table.resolve(path);
893 link.protocol
894 .delete(&resolved_path, recursive)
895 .await
896 .map(|r| r.result)
897 }
898
899 pub async fn trash(&self, path: &str) -> Result<Option<String>> {
903 if path.is_empty() {
904 return Err(HdfsError::InvalidPath("Empty path".to_string()));
905 }
906
907 let src_abs = self.absolute_path(path);
908 if !self.trash_enabled(&src_abs).await? {
909 return Err(HdfsError::TrashNotEnabled);
910 }
911
912 let trash_root = self.trash_root_path();
913
914 if Self::is_prefix_path(&trash_root, &src_abs) {
915 return Ok(None);
916 }
917 if Self::is_prefix_path(&src_abs, &trash_root) {
918 return Err(HdfsError::InvalidArgument(
919 "Cannot move to trash because it contains the trash".to_string(),
920 ));
921 }
922
923 let _ = self.get_file_info(&src_abs).await?;
924
925 let (src_parent, src_name) = Self::split_parent_name(&src_abs)?;
926 let trash_current = Self::join_paths(&trash_root, TRASH_CURRENT_DIR);
927 let src_parent_rel = src_parent.trim_start_matches('/');
928 let mut base_trash_path = if src_parent_rel.is_empty() {
929 trash_current.clone()
930 } else {
931 Self::join_paths(&trash_current, src_parent_rel)
932 };
933 let mut trash_path = Self::join_paths(&base_trash_path, &src_name);
934
935 for attempt in 0..2 {
936 let mut mkdirs_error: Option<HdfsError> = None;
937 loop {
938 match self
939 .mkdirs(&base_trash_path, TRASH_DIR_PERMISSION, true)
940 .await
941 {
942 Ok(()) => break,
943 Err(err) => {
944 if let Some(ancestor) = self.non_dir_ancestor(&base_trash_path).await? {
945 let timestamp = Self::current_time_millis();
946 base_trash_path = base_trash_path.replacen(
947 &ancestor,
948 &format!("{ancestor}{timestamp}"),
949 1,
950 );
951 trash_path = Self::join_paths(&base_trash_path, &src_name);
952 continue;
953 }
954 mkdirs_error = Some(err);
955 break;
956 }
957 }
958 }
959
960 if let Some(err) = mkdirs_error {
961 if attempt == 0 {
962 continue;
963 }
964 return Err(err);
965 }
966
967 let unique_trash_path = self.ensure_unique_trash_path(trash_path.clone()).await?;
968 match self
969 .rename_internal(&src_abs, &unique_trash_path, false, true)
970 .await
971 {
972 Ok(()) => return Ok(Some(unique_trash_path)),
973 Err(_) if attempt == 0 => continue,
974 Err(err) => return Err(err),
975 }
976 }
977
978 Err(HdfsError::OperationFailed(
979 "Failed to move to trash after retry".to_string(),
980 ))
981 }
982
983 pub async fn set_times(&self, path: &str, mtime: u64, atime: u64) -> Result<()> {
985 let (link, resolved_path) = self.mount_table.resolve(path);
986 link.protocol
987 .set_times(&resolved_path, mtime, atime)
988 .await?;
989 Ok(())
990 }
991
992 pub async fn set_owner(
994 &self,
995 path: &str,
996 owner: Option<&str>,
997 group: Option<&str>,
998 ) -> Result<()> {
999 let (link, resolved_path) = self.mount_table.resolve(path);
1000 link.protocol
1001 .set_owner(&resolved_path, owner, group)
1002 .await?;
1003 Ok(())
1004 }
1005
1006 pub async fn set_permission(&self, path: &str, permission: u32) -> Result<()> {
1011 let (link, resolved_path) = self.mount_table.resolve(path);
1012 link.protocol
1013 .set_permission(&resolved_path, permission)
1014 .await?;
1015 Ok(())
1016 }
1017
1018 pub async fn set_replication(&self, path: &str, replication: u32) -> Result<bool> {
1020 let (link, resolved_path) = self.mount_table.resolve(path);
1021 let result = link
1022 .protocol
1023 .set_replication(&resolved_path, replication)
1024 .await?
1025 .result;
1026
1027 Ok(result)
1028 }
1029
1030 pub async fn get_content_summary(&self, path: &str) -> Result<ContentSummary> {
1032 let (link, resolved_path) = self.mount_table.resolve(path);
1033 let result = link
1034 .protocol
1035 .get_content_summary(&resolved_path)
1036 .await?
1037 .summary;
1038
1039 Ok(result.into())
1040 }
1041
1042 pub async fn modify_acl_entries(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1044 let (link, resolved_path) = self.mount_table.resolve(path);
1045 link.protocol
1046 .modify_acl_entries(&resolved_path, acl_spec)
1047 .await?;
1048
1049 Ok(())
1050 }
1051
1052 pub async fn remove_acl_entries(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1054 let (link, resolved_path) = self.mount_table.resolve(path);
1055 link.protocol
1056 .remove_acl_entries(&resolved_path, acl_spec)
1057 .await?;
1058
1059 Ok(())
1060 }
1061
1062 pub async fn remove_default_acl(&self, path: &str) -> Result<()> {
1064 let (link, resolved_path) = self.mount_table.resolve(path);
1065 link.protocol.remove_default_acl(&resolved_path).await?;
1066
1067 Ok(())
1068 }
1069
1070 pub async fn remove_acl(&self, path: &str) -> Result<()> {
1072 let (link, resolved_path) = self.mount_table.resolve(path);
1073 link.protocol.remove_acl(&resolved_path).await?;
1074
1075 Ok(())
1076 }
1077
1078 pub async fn set_acl(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1082 let (link, resolved_path) = self.mount_table.resolve(path);
1083 link.protocol.set_acl(&resolved_path, acl_spec).await?;
1084
1085 Ok(())
1086 }
1087
1088 pub async fn get_acl_status(&self, path: &str) -> Result<AclStatus> {
1090 let (link, resolved_path) = self.mount_table.resolve(path);
1091 Ok(link
1092 .protocol
1093 .get_acl_status(&resolved_path)
1094 .await?
1095 .result
1096 .into())
1097 }
1098
1099 pub async fn glob_status(&self, pattern: &str) -> Result<Vec<FileStatus>> {
1102 let flattened = expand_glob(pattern.to_string())?;
1104
1105 let mut results: Vec<FileStatus> = Vec::new();
1106
1107 for flat in flattened.into_iter() {
1108 if flat.is_empty() {
1111 continue;
1112 }
1113
1114 let components = get_path_components(&flat);
1115
1116 #[derive(Clone, Debug)]
1118 struct Candidate {
1119 path: String,
1120 status: Option<FileStatus>,
1121 }
1122
1123 let mut candidates: Vec<Candidate> = vec![Candidate {
1125 path: "/".to_string(),
1126 status: None,
1127 }];
1128
1129 for (idx, comp) in components.iter().enumerate() {
1130 if candidates.is_empty() {
1131 break;
1132 }
1133
1134 let is_last = idx == components.len() - 1;
1135
1136 let unescaped = unescape_component(comp);
1137 let glob_pat = GlobPattern::new(comp)?;
1138
1139 if !is_last && !glob_pat.has_wildcard() {
1140 for cand in candidates.iter_mut() {
1142 if !cand.path.ends_with('/') {
1143 cand.path.push('/');
1144 }
1145 cand.path.push_str(&unescaped);
1146 }
1148 continue;
1149 }
1150
1151 let mut new_candidates: Vec<Candidate> = Vec::new();
1152
1153 for cand in candidates.into_iter() {
1154 if glob_pat.has_wildcard() {
1155 let listing = match self.list_status(&cand.path, false).await {
1157 Ok(listing) => listing,
1158 Err(HdfsError::FileNotFound(_)) => continue,
1159 Err(e) => return Err(e),
1160 };
1161 if listing.len() == 1 && listing[0].path == cand.path {
1162 continue;
1164 }
1165
1166 for child in listing.into_iter() {
1167 if !is_last && !child.isdir {
1169 continue;
1170 }
1171
1172 let name = child
1175 .path
1176 .rsplit_once('/')
1177 .map(|(_, n)| n)
1178 .unwrap_or(child.path.as_str());
1179
1180 if glob_pat.matches(name) {
1181 new_candidates.push(Candidate {
1182 path: child.path.clone(),
1183 status: Some(child),
1184 });
1185 }
1186 }
1187 } else {
1188 let mut next_path = cand.path.clone();
1190 if !next_path.ends_with('/') {
1191 next_path.push('/');
1192 }
1193 next_path.push_str(&unescaped);
1194
1195 match self.get_file_info(&next_path).await {
1196 Ok(status) => {
1197 if is_last || status.isdir {
1198 new_candidates.push(Candidate {
1199 path: status.path.clone(),
1200 status: Some(status),
1201 });
1202 }
1203 }
1204 Err(HdfsError::FileNotFound(_)) => continue,
1205 Err(e) => return Err(e),
1206 }
1207 }
1208 }
1209
1210 candidates = new_candidates;
1211 }
1212
1213 for cand in candidates.into_iter() {
1215 let status = if let Some(s) = cand.status {
1216 s
1217 } else {
1218 match self.get_file_info(&cand.path).await {
1220 Ok(s) => s,
1221 Err(HdfsError::FileNotFound(_)) => continue,
1222 Err(e) => return Err(e),
1223 }
1224 };
1225
1226 results.push(status);
1227 }
1228 }
1229
1230 Ok(results)
1231 }
1232}
1233
1234impl Default for Client {
1235 fn default() -> Self {
1238 ClientBuilder::new()
1239 .build()
1240 .expect("Failed to create default client")
1241 }
1242}
1243
1244pub(crate) struct DirListingIterator {
1245 path: String,
1246 resolved_path: String,
1247 link: MountLink,
1248 files_only: bool,
1249 partial_listing: VecDeque<HdfsFileStatusProto>,
1250 remaining: u32,
1251 last_seen: Vec<u8>,
1252}
1253
1254impl DirListingIterator {
1255 fn new(path: String, mount_table: &Arc<MountTable>, files_only: bool) -> Self {
1256 let (link, resolved_path) = mount_table.resolve(&path);
1257
1258 DirListingIterator {
1259 path,
1260 resolved_path,
1261 link: link.clone(),
1262 files_only,
1263 partial_listing: VecDeque::new(),
1264 remaining: 1,
1265 last_seen: Vec::new(),
1266 }
1267 }
1268
1269 async fn get_next_batch(&mut self) -> Result<bool> {
1270 let listing = self
1271 .link
1272 .protocol
1273 .get_listing(&self.resolved_path, self.last_seen.clone(), false)
1274 .await?;
1275
1276 if let Some(dir_list) = listing.dir_list {
1277 self.last_seen = dir_list
1278 .partial_listing
1279 .last()
1280 .map(|p| p.path.clone())
1281 .unwrap_or(Vec::new());
1282
1283 self.remaining = dir_list.remaining_entries;
1284
1285 self.partial_listing = dir_list
1286 .partial_listing
1287 .into_iter()
1288 .filter(|s| !self.files_only || s.file_type() != FileType::IsDir)
1289 .collect();
1290 Ok(!self.partial_listing.is_empty())
1291 } else {
1292 Err(HdfsError::FileNotFound(self.path.clone()))
1293 }
1294 }
1295
1296 pub async fn next(&mut self) -> Option<Result<FileStatus>> {
1297 if self.partial_listing.is_empty()
1298 && self.remaining > 0
1299 && let Err(error) = self.get_next_batch().await
1300 {
1301 self.remaining = 0;
1302 return Some(Err(error));
1303 }
1304 if let Some(next) = self.partial_listing.pop_front() {
1305 Some(Ok(FileStatus::from(next, &self.path)))
1306 } else {
1307 None
1308 }
1309 }
1310}
1311
1312pub struct ListStatusIterator {
1313 mount_table: Arc<MountTable>,
1314 recursive: bool,
1315 iters: Arc<tokio::sync::Mutex<Vec<DirListingIterator>>>,
1316}
1317
1318impl ListStatusIterator {
1319 fn new(path: String, mount_table: Arc<MountTable>, recursive: bool) -> Self {
1320 let initial = DirListingIterator::new(path.clone(), &mount_table, false);
1321
1322 ListStatusIterator {
1323 mount_table,
1324 recursive,
1325 iters: Arc::new(tokio::sync::Mutex::new(vec![initial])),
1326 }
1327 }
1328
1329 pub async fn next(&self) -> Option<Result<FileStatus>> {
1330 let mut next_file: Option<Result<FileStatus>> = None;
1331 let mut iters = self.iters.lock().await;
1332 while next_file.is_none() {
1333 if let Some(iter) = iters.last_mut() {
1334 if let Some(file_result) = iter.next().await {
1335 if let Ok(file) = file_result {
1336 if file.isdir && self.recursive {
1339 iters.push(DirListingIterator::new(
1340 file.path.clone(),
1341 &self.mount_table,
1342 false,
1343 ))
1344 }
1345 next_file = Some(Ok(file));
1346 } else {
1347 next_file = Some(file_result)
1349 }
1350 } else {
1351 iters.pop();
1353 }
1354 } else {
1355 break;
1357 }
1358 }
1359
1360 next_file
1361 }
1362
1363 pub fn into_stream(self) -> BoxStream<'static, Result<FileStatus>> {
1364 let listing = stream::unfold(self, |state| async move {
1365 let next = state.next().await;
1366 next.map(|n| (n, state))
1367 });
1368 Box::pin(listing)
1369 }
1370}
1371
1372#[derive(Debug, Clone)]
1373pub struct FileStatus {
1374 pub path: String,
1375 pub length: usize,
1376 pub isdir: bool,
1377 pub permission: u16,
1378 pub owner: String,
1379 pub group: String,
1380 pub modification_time: u64,
1381 pub access_time: u64,
1382 pub replication: Option<u32>,
1383 pub blocksize: Option<u64>,
1384}
1385
1386impl FileStatus {
1387 fn from(value: HdfsFileStatusProto, base_path: &str) -> Self {
1388 let mut path = base_path.trim_end_matches("/").to_string();
1389 let relative_path = std::str::from_utf8(&value.path).unwrap();
1390 if !relative_path.is_empty() {
1391 path.push('/');
1392 path.push_str(relative_path);
1393 }
1394
1395 if path.is_empty() {
1397 path.push('/');
1398 }
1399
1400 FileStatus {
1401 isdir: value.file_type() == FileType::IsDir,
1402 path,
1403 length: value.length as usize,
1404 permission: value.permission.perm as u16,
1405 owner: value.owner,
1406 group: value.group,
1407 modification_time: value.modification_time,
1408 access_time: value.access_time,
1409 replication: value.block_replication,
1410 blocksize: value.blocksize,
1411 }
1412 }
1413}
1414
1415#[derive(Debug)]
1416pub struct ContentSummary {
1417 pub length: u64,
1418 pub file_count: u64,
1419 pub directory_count: u64,
1420 pub quota: u64,
1421 pub space_consumed: u64,
1422 pub space_quota: u64,
1423}
1424
1425impl From<ContentSummaryProto> for ContentSummary {
1426 fn from(value: ContentSummaryProto) -> Self {
1427 ContentSummary {
1428 length: value.length,
1429 file_count: value.file_count,
1430 directory_count: value.directory_count,
1431 quota: value.quota,
1432 space_consumed: value.space_consumed,
1433 space_quota: value.space_quota,
1434 }
1435 }
1436}
1437
1438#[cfg(test)]
1439mod test {
1440 use std::sync::{Arc, LazyLock};
1441
1442 use tokio::runtime::Runtime;
1443 use url::Url;
1444
1445 use crate::{
1446 client::ClientBuilder,
1447 common::config::Configuration,
1448 hdfs::{protocol::NamenodeProtocol, proxy::NameServiceProxy},
1449 };
1450
1451 use super::{MountLink, MountTable};
1452
1453 static RT: LazyLock<Runtime> = LazyLock::new(|| Runtime::new().unwrap());
1454
1455 fn create_protocol(url: &str) -> Arc<NamenodeProtocol> {
1456 let proxy = NameServiceProxy::new(
1457 &Url::parse(url).unwrap(),
1458 Arc::new(Configuration::new(None, None).unwrap()),
1459 RT.handle().clone(),
1460 None,
1461 )
1462 .unwrap();
1463 Arc::new(NamenodeProtocol::new(proxy, RT.handle().clone()))
1464 }
1465
1466 #[test]
1467 fn test_default_fs() {
1468 assert!(
1469 ClientBuilder::new()
1470 .with_config(vec![("fs.defaultFS", "hdfs://test:9000")])
1471 .build()
1472 .is_ok()
1473 );
1474
1475 assert!(
1476 ClientBuilder::new()
1477 .with_config(vec![("fs.defaultFS", "hdfs://")])
1478 .build()
1479 .is_err()
1480 );
1481
1482 assert!(
1483 ClientBuilder::new()
1484 .with_url("hdfs://")
1485 .with_config(vec![("fs.defaultFS", "hdfs://test:9000")])
1486 .build()
1487 .is_ok()
1488 );
1489
1490 assert!(
1491 ClientBuilder::new()
1492 .with_url("hdfs://")
1493 .with_config(vec![("fs.defaultFS", "hdfs://")])
1494 .build()
1495 .is_err()
1496 );
1497
1498 assert!(
1499 ClientBuilder::new()
1500 .with_url("hdfs://")
1501 .with_config(vec![("fs.defaultFS", "viewfs://test")])
1502 .build()
1503 .is_err()
1504 );
1505 }
1506
1507 #[test]
1508 fn test_mount_link_resolve() {
1509 let protocol = create_protocol("hdfs://127.0.0.1:9000");
1510 let link = MountLink::new("/view", "/hdfs", protocol);
1511
1512 assert_eq!(link.resolve("/view/dir/file").unwrap(), "/hdfs/dir/file");
1513 assert_eq!(link.resolve("/view").unwrap(), "/hdfs");
1514 assert!(link.resolve("/hdfs/path").is_none());
1515 }
1516
1517 #[test]
1518 fn test_fallback_link() {
1519 let protocol = create_protocol("hdfs://127.0.0.1:9000");
1520 let link = MountLink::new("", "/hdfs", Arc::clone(&protocol));
1521
1522 assert_eq!(link.resolve("/path/to/file").unwrap(), "/hdfs/path/to/file");
1523 assert_eq!(link.resolve("/").unwrap(), "/hdfs/");
1524 assert_eq!(link.resolve("/hdfs/path").unwrap(), "/hdfs/hdfs/path");
1525
1526 let link = MountLink::new("", "", protocol);
1527 assert_eq!(link.resolve("/").unwrap(), "/");
1528 }
1529
1530 #[test]
1531 fn test_mount_table_resolve() {
1532 let link1 = MountLink::new(
1533 "/mount1",
1534 "/path1/nested",
1535 create_protocol("hdfs://127.0.0.1:9000"),
1536 );
1537 let link2 = MountLink::new(
1538 "/mount2",
1539 "/path2",
1540 create_protocol("hdfs://127.0.0.1:9001"),
1541 );
1542 let link3 = MountLink::new(
1543 "/mount3/nested",
1544 "/path3",
1545 create_protocol("hdfs://127.0.0.1:9002"),
1546 );
1547 let fallback = MountLink::new("/", "/path4", create_protocol("hdfs://127.0.0.1:9003"));
1548
1549 let mount_table = MountTable {
1550 mounts: vec![link1, link2, link3],
1551 fallback,
1552 home_dir: "/user/test".to_string(),
1553 };
1554
1555 let (link, resolved) = mount_table.resolve("/mount1");
1557 assert_eq!(link.viewfs_path, "/mount1");
1558 assert_eq!(resolved, "/path1/nested");
1559
1560 let (link, resolved) = mount_table.resolve("/mount1/");
1562 assert_eq!(link.viewfs_path, "/mount1");
1563 assert_eq!(resolved, "/path1/nested/");
1564
1565 let (link, resolved) = mount_table.resolve("/mount12");
1567 assert_eq!(link.viewfs_path, "");
1568 assert_eq!(resolved, "/path4/mount12");
1569
1570 let (link, resolved) = mount_table.resolve("/mount3/file");
1571 assert_eq!(link.viewfs_path, "");
1572 assert_eq!(resolved, "/path4/mount3/file");
1573
1574 let (link, resolved) = mount_table.resolve("/mount3/nested/file");
1575 assert_eq!(link.viewfs_path, "/mount3/nested");
1576 assert_eq!(resolved, "/path3/file");
1577
1578 let (link, resolved) = mount_table.resolve("file");
1579 assert_eq!(link.viewfs_path, "");
1580 assert_eq!(resolved, "/path4/user/test/file");
1581
1582 let (link, resolved) = mount_table.resolve("dir/subdir");
1583 assert_eq!(link.viewfs_path, "");
1584 assert_eq!(resolved, "/path4/user/test/dir/subdir");
1585
1586 let mount_table = MountTable {
1587 mounts: vec![
1588 MountLink::new(
1589 "/mount1",
1590 "/path1/nested",
1591 create_protocol("hdfs://127.0.0.1:9000"),
1592 ),
1593 MountLink::new(
1594 "/mount2",
1595 "/path2",
1596 create_protocol("hdfs://127.0.0.1:9001"),
1597 ),
1598 ],
1599 fallback: MountLink::new("/", "/path4", create_protocol("hdfs://127.0.0.1:9003")),
1600 home_dir: "/mount1/user".to_string(),
1601 };
1602
1603 let (link, resolved) = mount_table.resolve("file");
1604 assert_eq!(link.viewfs_path, "/mount1");
1605 assert_eq!(resolved, "/path1/nested/user/file");
1606
1607 let (link, resolved) = mount_table.resolve("dir/subdir");
1608 assert_eq!(link.viewfs_path, "/mount1");
1609 assert_eq!(resolved, "/path1/nested/user/dir/subdir");
1610 }
1611
1612 #[test]
1613 fn test_io_runtime() {
1614 assert!(
1615 ClientBuilder::new()
1616 .with_url("hdfs://127.0.0.1:9000")
1617 .with_io_runtime(Runtime::new().unwrap())
1618 .build()
1619 .is_ok()
1620 );
1621
1622 let rt = Runtime::new().unwrap();
1623 assert!(
1624 ClientBuilder::new()
1625 .with_url("hdfs://127.0.0.1:9000")
1626 .with_io_runtime(rt.handle().clone())
1627 .build()
1628 .is_ok()
1629 );
1630 }
1631
1632 #[test]
1633 fn test_with_user_sets_relative_path_home_dir() {
1634 let client = ClientBuilder::new()
1635 .with_url("hdfs://127.0.0.1:9000")
1636 .with_user("alice")
1637 .build()
1638 .unwrap();
1639
1640 let (_, resolved) = client.mount_table.resolve("file");
1641 assert_eq!(resolved, "/user/alice/file");
1642 }
1643
1644 #[test]
1645 fn test_set_conf_dir() {
1646 assert!(
1647 ClientBuilder::new()
1648 .with_url("hdfs://127.0.0.1:9000")
1649 .with_config_dir("target/test")
1650 .build()
1651 .is_ok()
1652 )
1653 }
1654}