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)]
292pub struct ClientBuilder {
293 url: Option<String>,
294 config: Option<HashMap<String, String>>,
295 config_dir: Option<String>,
296 runtime: Option<IORuntime>,
297 user: Option<String>,
298 kerberos_principal: Option<String>,
299 kerberos_keytab: Option<String>,
300 kerberos_cache: Option<String>,
301}
302
303impl ClientBuilder {
304 pub fn new() -> Self {
306 Self::default()
307 }
308
309 pub fn with_url(mut self, url: impl Into<String>) -> Self {
311 self.url = Some(url.into());
312 self
313 }
314
315 pub fn with_config(
317 mut self,
318 config: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
319 ) -> Self {
320 self.config = Some(
321 config
322 .into_iter()
323 .map(|(k, v)| (k.into(), v.into()))
324 .collect(),
325 );
326 self
327 }
328
329 pub fn with_config_dir(mut self, config_dir: impl Into<String>) -> Self {
331 self.config_dir = Some(config_dir.into());
332 self
333 }
334
335 pub fn with_io_runtime(mut self, runtime: impl Into<IORuntime>) -> Self {
338 self.runtime = Some(runtime.into());
339 self
340 }
341
342 pub fn with_user(mut self, user: impl Into<String>) -> Self {
344 self.user = Some(user.into());
345 self
346 }
347
348 pub fn with_kerberos_principal(mut self, principal: impl Into<String>) -> Self {
350 self.kerberos_principal = Some(principal.into());
351 self
352 }
353
354 pub fn with_kerberos_keytab(mut self, keytab: impl Into<String>) -> Self {
356 self.kerberos_keytab = Some(keytab.into());
357 self
358 }
359
360 pub fn with_kerberos_cache(mut self, cache: impl Into<String>) -> Self {
362 self.kerberos_cache = Some(cache.into());
363 self
364 }
365
366 pub fn build(self) -> Result<Client> {
368 let config = Configuration::new(self.config_dir, self.config)?;
369 let url = if let Some(url) = self.url {
370 Url::parse(&url)?
371 } else {
372 Client::default_fs(&config)?
373 };
374
375 let kerberos_credentials = crate::security::KerberosCredentials::new(
376 self.kerberos_principal,
377 self.kerberos_keytab,
378 self.kerberos_cache,
379 )?
380 .map(crate::security::ClientAuth::new);
381
382 Client::build(&url, config, self.runtime, self.user, kerberos_credentials)
383 }
384}
385
386#[derive(Clone, Debug)]
387enum RuntimeHolder {
388 Custom(Arc<IORuntime>),
389 Default(Arc<OnceLock<Runtime>>),
390}
391
392impl RuntimeHolder {
393 fn new(rt: Option<IORuntime>) -> Self {
394 if let Some(rt) = rt {
395 Self::Custom(Arc::new(rt))
396 } else {
397 Self::Default(Arc::new(OnceLock::new()))
398 }
399 }
400
401 fn get_handle(&self) -> Handle {
402 match self {
403 Self::Custom(rt) => rt.handle().clone(),
404 Self::Default(rt) => match Handle::try_current() {
405 Ok(handle) => handle,
406 Err(_) => rt
407 .get_or_init(|| Runtime::new().expect("Failed to create tokio runtime"))
408 .handle()
409 .clone(),
410 },
411 }
412 }
413}
414
415#[derive(Clone, Debug)]
417pub struct Client {
418 mount_table: Arc<MountTable>,
419 config: Arc<Configuration>,
420 rt_holder: RuntimeHolder,
423 #[cfg(feature = "kms")]
426 kms_client: Option<Arc<KmsClient>>,
427}
428
429impl Client {
430 fn default_fs(config: &Configuration) -> Result<Url> {
431 let url = config
432 .get(config::DEFAULT_FS)
433 .ok_or(HdfsError::InvalidArgument(format!(
434 "No {} setting found",
435 config::DEFAULT_FS
436 )))?;
437 Ok(Url::parse(url)?)
438 }
439
440 fn build(
441 url: &Url,
442 config: Configuration,
443 rt: Option<IORuntime>,
444 user: Option<String>,
445 auth: Option<Arc<crate::security::ClientAuth>>,
446 ) -> Result<Self> {
447 let resolved_url = if !url.has_host() {
448 let default_url = Self::default_fs(&config)?;
449 if url.scheme() != default_url.scheme() || !default_url.has_host() {
450 return Err(HdfsError::InvalidArgument(
451 "URL must contain a host".to_string(),
452 ));
453 }
454 default_url
455 } else {
456 url.clone()
457 };
458
459 let config = Arc::new(config);
460
461 let rt_holder = RuntimeHolder::new(rt);
462
463 let user_info = if config.security_enabled()
464 && let Some(principal) = auth
465 .as_deref()
466 .and_then(|auth| auth.credentials())
467 .and_then(|credentials| credentials.principal.as_deref())
468 {
469 User::get_user_info_from_principal(principal, user.clone())
470 } else {
471 User::get_user_info(user.clone(), config.security_enabled())
472 };
473 let username = user_info
474 .effective_user
475 .as_deref()
476 .or(user_info.real_user.as_deref())
477 .expect("User info must include a username");
478 let home_dir = build_home_dir(
479 resolved_url.scheme(),
480 resolved_url.host_str(),
481 config.as_ref(),
482 username,
483 );
484
485 let mount_table = match url.scheme() {
486 "hdfs" => {
487 let proxy = NameServiceProxy::new(
488 &resolved_url,
489 Arc::clone(&config),
490 rt_holder.get_handle(),
491 user.clone(),
492 auth.clone(),
493 )?;
494 let protocol = Arc::new(NamenodeProtocol::new(proxy, rt_holder.get_handle()));
495
496 MountTable {
497 mounts: Vec::new(),
498 fallback: MountLink::new("/", "/", protocol),
499 home_dir,
500 }
501 }
502 "viewfs" => Self::build_mount_table(
503 resolved_url.host_str().expect("URL must have a host"),
505 Arc::clone(&config),
506 rt_holder.get_handle(),
507 user.clone(),
508 auth.clone(),
509 home_dir,
510 )?,
511 _ => {
512 return Err(HdfsError::InvalidArgument(
513 "Only `hdfs` and `viewfs` schemes are supported".to_string(),
514 ));
515 }
516 };
517
518 #[cfg(feature = "kms")]
519 let kms_client =
520 KmsClient::from_config(config.as_ref(), None, username.to_string(), auth.clone())?;
521
522 Ok(Self {
523 mount_table: Arc::new(mount_table),
524 config,
525 rt_holder,
526 #[cfg(feature = "kms")]
527 kms_client,
528 })
529 }
530
531 fn build_mount_table(
532 host: &str,
533 config: Arc<Configuration>,
534 handle: Handle,
535 effective_user: Option<String>,
536 auth: Option<Arc<crate::security::ClientAuth>>,
537 home_dir: String,
538 ) -> Result<MountTable> {
539 let mut mounts: Vec<MountLink> = Vec::new();
540 let mut fallback: Option<MountLink> = None;
541
542 for (viewfs_path, hdfs_url) in config.get_mount_table(host).iter() {
543 let url = Url::parse(hdfs_url)?;
544 if !url.has_host() {
545 return Err(HdfsError::InvalidArgument(
546 "URL must contain a host".to_string(),
547 ));
548 }
549 if url.scheme() != "hdfs" {
550 return Err(HdfsError::InvalidArgument(
551 "Only hdfs mounts are supported for viewfs".to_string(),
552 ));
553 }
554 let proxy = NameServiceProxy::new(
555 &url,
556 Arc::clone(&config),
557 handle.clone(),
558 effective_user.clone(),
559 auth.clone(),
560 )?;
561 let protocol = Arc::new(NamenodeProtocol::new(proxy, handle.clone()));
562
563 if let Some(prefix) = viewfs_path {
564 mounts.push(MountLink::new(prefix, url.path(), protocol));
565 } else {
566 if fallback.is_some() {
567 return Err(HdfsError::InvalidArgument(
568 "Multiple viewfs fallback links found".to_string(),
569 ));
570 }
571 fallback = Some(MountLink::new("/", url.path(), protocol));
572 }
573 }
574
575 if let Some(fallback) = fallback {
576 mounts.sort_by_key(|m| m.viewfs_path.chars().filter(|c| *c == '/').count());
578 mounts.reverse();
579
580 Ok(MountTable {
581 mounts,
582 fallback,
583 home_dir,
584 })
585 } else {
586 Err(HdfsError::InvalidArgument(
587 "No viewfs fallback mount found".to_string(),
588 ))
589 }
590 }
591
592 fn normalize_path(path: &str) -> String {
593 let mut normalized = if path.is_empty() {
594 "/".to_string()
595 } else {
596 path.to_string()
597 };
598 if !normalized.starts_with('/') {
599 normalized.insert(0, '/');
600 }
601 while normalized.len() > 1 && normalized.ends_with('/') {
602 normalized.pop();
603 }
604 normalized
605 }
606
607 fn join_paths(base: &str, suffix: &str) -> String {
608 if suffix.is_empty() {
609 return base.to_string();
610 }
611 let trimmed_base = if base.is_empty() { "/" } else { base };
612 let suffix = suffix.trim_start_matches('/');
613 if trimmed_base == "/" {
614 format!("/{suffix}")
615 } else {
616 format!("{}/{}", trimmed_base.trim_end_matches('/'), suffix)
617 }
618 }
619
620 fn is_prefix_path(parent: &str, child: &str) -> bool {
621 if parent == "/" {
622 return true;
623 }
624 child == parent || child.starts_with(&format!("{parent}/"))
625 }
626
627 fn current_time_millis() -> u64 {
628 SystemTime::now()
629 .duration_since(UNIX_EPOCH)
630 .unwrap_or_default()
631 .as_millis() as u64
632 }
633
634 fn absolute_path(&self, path: &str) -> String {
635 if path.starts_with('/') {
636 Self::normalize_path(path)
637 } else {
638 let home = self.mount_table.home_dir.trim_end_matches('/');
639 Self::normalize_path(&format!("{home}/{path}"))
640 }
641 }
642
643 fn trash_root_path(&self) -> String {
644 let home = Self::normalize_path(&self.mount_table.home_dir);
645 Self::join_paths(&home, TRASH_ROOT_DIR)
646 }
647
648 async fn trash_enabled(&self, path: &str) -> Result<bool> {
649 let (link, _) = self.mount_table.resolve(path);
650 let server_defaults = link.protocol.get_cached_server_defaults().await?;
651 Ok(server_defaults.trash_interval.unwrap_or_default() > 0)
652 }
653
654 fn split_parent_name(path: &str) -> Result<(String, String)> {
655 let normalized = Self::normalize_path(path);
656 if normalized == "/" {
657 return Err(HdfsError::InvalidArgument(
658 "Cannot move the root directory to trash".to_string(),
659 ));
660 }
661 let (parent, name) = normalized
662 .rsplit_once('/')
663 .expect("Normalized path always contains '/'");
664 let parent = if parent.is_empty() {
665 "/".to_string()
666 } else {
667 parent.to_string()
668 };
669 Ok((parent, name.to_string()))
670 }
671
672 async fn non_dir_ancestor(&self, path: &str) -> Result<Option<String>> {
673 let normalized = Self::normalize_path(path);
674 let mut current = "/".to_string();
675 for component in normalized.trim_start_matches('/').split('/') {
676 if component.is_empty() {
677 continue;
678 }
679 current = Self::join_paths(¤t, component);
680 match self.get_file_info(¤t).await {
681 Ok(status) => {
682 if !status.isdir {
683 return Ok(Some(current));
684 }
685 }
686 Err(HdfsError::FileNotFound(_)) => return Ok(None),
687 Err(err) => return Err(err),
688 }
689 }
690 Ok(None)
691 }
692
693 async fn ensure_unique_trash_path(&self, path: String) -> Result<String> {
694 let base = path.clone();
695 let mut candidate = path;
696 loop {
697 match self.get_file_info(&candidate).await {
698 Ok(_) => {
699 candidate = format!("{}{}", base, Self::current_time_millis());
700 }
701 Err(HdfsError::FileNotFound(_)) => return Ok(candidate),
702 Err(err) => return Err(err),
703 }
704 }
705 }
706
707 pub async fn get_file_info(&self, path: &str) -> Result<FileStatus> {
709 let (link, resolved_path) = self.mount_table.resolve(path);
710 match link.protocol.get_file_info(&resolved_path).await?.fs {
711 Some(status) => Ok(FileStatus::from(status, path)),
712 None => Err(HdfsError::FileNotFound(path.to_string())),
713 }
714 }
715
716 pub async fn list_status(&self, path: &str, recursive: bool) -> Result<Vec<FileStatus>> {
719 let iter = self.list_status_iter(path, recursive);
720 let statuses = iter
721 .into_stream()
722 .collect::<Vec<Result<FileStatus>>>()
723 .await;
724
725 let mut resolved_statues = Vec::<FileStatus>::with_capacity(statuses.len());
726 for status in statuses.into_iter() {
727 resolved_statues.push(status?);
728 }
729
730 Ok(resolved_statues)
731 }
732
733 pub fn list_status_iter(&self, path: &str, recursive: bool) -> ListStatusIterator {
735 ListStatusIterator::new(path.to_string(), Arc::clone(&self.mount_table), recursive)
736 }
737
738 pub async fn read(&self, path: &str) -> Result<FileReader> {
740 let (link, resolved_path) = self.mount_table.resolve(path);
741 let located_info = link
743 .protocol
744 .get_block_locations(&resolved_path, 0, i64::MAX as u64)
745 .await?;
746
747 if let Some(locations) = located_info.locations {
748 let ec_schema = if let Some(ec_policy) = locations.ec_policy.as_ref() {
749 Some(resolve_ec_policy(ec_policy)?)
750 } else {
751 None
752 };
753
754 let crypto = self
755 .build_crypto_codec(locations.file_encryption_info.as_ref())
756 .await?;
757
758 Ok(FileReader::new(
759 Arc::clone(&link.protocol),
760 locations,
761 ec_schema,
762 Arc::clone(&self.config),
763 self.rt_holder.get_handle(),
764 crypto,
765 ))
766 } else {
767 Err(HdfsError::FileNotFound(path.to_string()))
768 }
769 }
770
771 async fn build_crypto_codec(
775 &self,
776 info: Option<&FileEncryptionInfoProto>,
777 ) -> Result<Option<Arc<FileCryptoCodec>>> {
778 let Some(info) = info else {
779 return Ok(None);
780 };
781 #[cfg(feature = "kms")]
782 {
783 let kms = self.kms_client.as_ref().ok_or_else(|| {
784 HdfsError::OperationFailed(
785 "File is in an HDFS encryption zone but no KMS provider is configured \
786 (set `hadoop.security.key.provider.path` in core-site.xml)"
787 .to_string(),
788 )
789 })?;
790 let dek = kms.decrypt_edek(info).await?;
791 Ok(Some(Arc::new(FileCryptoCodec::new(info, dek)?)))
792 }
793 #[cfg(not(feature = "kms"))]
794 {
795 let _ = info;
796 Err(HdfsError::UnsupportedFeature(
797 "file is in an HDFS encryption zone; reading or writing it requires \
798 building hdfs-native with the `kms` cargo feature"
799 .to_string(),
800 ))
801 }
802 }
803
804 pub async fn create(
807 &self,
808 src: &str,
809 write_options: impl AsRef<WriteOptions>,
810 ) -> Result<FileWriter> {
811 let write_options = write_options.as_ref();
812
813 let (link, resolved_path) = self.mount_table.resolve(src);
814
815 let create_response = link
816 .protocol
817 .create(
818 &resolved_path,
819 write_options.permission,
820 write_options.overwrite,
821 write_options.create_parent,
822 write_options.replication,
823 write_options.block_size,
824 )
825 .await?;
826
827 match create_response.fs {
828 Some(status) => {
829 let crypto = match self
830 .build_crypto_codec(status.file_encryption_info.as_ref())
831 .await
832 {
833 Ok(c) => c,
834 Err(e) => {
835 let _ = self.delete(src, false).await;
839 return Err(e);
840 }
841 };
842
843 Ok(FileWriter::new(
844 Arc::clone(&link.protocol),
845 resolved_path,
846 status,
847 Arc::clone(&self.config),
848 self.rt_holder.get_handle(),
849 None,
850 crypto,
851 ))
852 }
853 None => Err(HdfsError::FileNotFound(src.to_string())),
854 }
855 }
856
857 fn needs_new_block(class: &str, msg: &str) -> bool {
858 class == "java.lang.UnsupportedOperationException" && msg.contains("NEW_BLOCK")
859 }
860
861 pub async fn append(&self, src: &str) -> Result<FileWriter> {
865 let (link, resolved_path) = self.mount_table.resolve(src);
866
867 let append_response = match link.protocol.append(&resolved_path, false).await {
870 Err(HdfsError::RPCError(class, msg)) if Self::needs_new_block(&class, &msg) => {
871 link.protocol.append(&resolved_path, true).await?
872 }
873 resp => resp?,
874 };
875
876 match append_response.stat {
877 Some(status) => {
878 let crypto = match self
879 .build_crypto_codec(status.file_encryption_info.as_ref())
880 .await
881 {
882 Ok(c) => c,
883 Err(e) => {
884 let _ = link
887 .protocol
888 .complete(
889 src,
890 append_response.block.as_ref().map(|b| b.b.clone()),
891 status.file_id,
892 )
893 .await;
894 return Err(e);
895 }
896 };
897
898 Ok(FileWriter::new(
899 Arc::clone(&link.protocol),
900 resolved_path,
901 status,
902 Arc::clone(&self.config),
903 self.rt_holder.get_handle(),
904 append_response.block,
905 crypto,
906 ))
907 }
908 None => Err(HdfsError::FileNotFound(src.to_string())),
909 }
910 }
911
912 pub async fn mkdirs(&self, path: &str, permission: u32, create_parent: bool) -> Result<()> {
920 let (link, resolved_path) = self.mount_table.resolve(path);
921 link.protocol
922 .mkdirs(&resolved_path, permission, create_parent)
923 .await
924 .map(|_| ())
925 }
926
927 async fn rename_internal(
928 &self,
929 src: &str,
930 dst: &str,
931 overwrite: bool,
932 move_to_trash: bool,
933 ) -> Result<()> {
934 let (src_link, src_resolved_path) = self.mount_table.resolve(src);
935 let (dst_link, dst_resolved_path) = self.mount_table.resolve(dst);
936 if src_link.viewfs_path == dst_link.viewfs_path {
937 src_link
938 .protocol
939 .rename(
940 &src_resolved_path,
941 &dst_resolved_path,
942 overwrite,
943 move_to_trash,
944 )
945 .await
946 .map(|_| ())
947 } else {
948 Err(HdfsError::InvalidArgument(
949 "Cannot rename across different name services".to_string(),
950 ))
951 }
952 }
953
954 pub async fn rename(&self, src: &str, dst: &str, overwrite: bool) -> Result<()> {
956 self.rename_internal(src, dst, overwrite, false).await
957 }
958
959 pub async fn delete(&self, path: &str, recursive: bool) -> Result<bool> {
962 let (link, resolved_path) = self.mount_table.resolve(path);
963 link.protocol
964 .delete(&resolved_path, recursive)
965 .await
966 .map(|r| r.result)
967 }
968
969 pub async fn trash(&self, path: &str) -> Result<Option<String>> {
973 if path.is_empty() {
974 return Err(HdfsError::InvalidPath("Empty path".to_string()));
975 }
976
977 let src_abs = self.absolute_path(path);
978 if !self.trash_enabled(&src_abs).await? {
979 return Err(HdfsError::TrashNotEnabled);
980 }
981
982 let trash_root = self.trash_root_path();
983
984 if Self::is_prefix_path(&trash_root, &src_abs) {
985 return Ok(None);
986 }
987 if Self::is_prefix_path(&src_abs, &trash_root) {
988 return Err(HdfsError::InvalidArgument(
989 "Cannot move to trash because it contains the trash".to_string(),
990 ));
991 }
992
993 let _ = self.get_file_info(&src_abs).await?;
994
995 let (src_parent, src_name) = Self::split_parent_name(&src_abs)?;
996 let trash_current = Self::join_paths(&trash_root, TRASH_CURRENT_DIR);
997 let src_parent_rel = src_parent.trim_start_matches('/');
998 let mut base_trash_path = if src_parent_rel.is_empty() {
999 trash_current.clone()
1000 } else {
1001 Self::join_paths(&trash_current, src_parent_rel)
1002 };
1003 let mut trash_path = Self::join_paths(&base_trash_path, &src_name);
1004
1005 for attempt in 0..2 {
1006 let mut mkdirs_error: Option<HdfsError> = None;
1007 loop {
1008 match self
1009 .mkdirs(&base_trash_path, TRASH_DIR_PERMISSION, true)
1010 .await
1011 {
1012 Ok(()) => break,
1013 Err(err) => {
1014 if let Some(ancestor) = self.non_dir_ancestor(&base_trash_path).await? {
1015 let timestamp = Self::current_time_millis();
1016 base_trash_path = base_trash_path.replacen(
1017 &ancestor,
1018 &format!("{ancestor}{timestamp}"),
1019 1,
1020 );
1021 trash_path = Self::join_paths(&base_trash_path, &src_name);
1022 continue;
1023 }
1024 mkdirs_error = Some(err);
1025 break;
1026 }
1027 }
1028 }
1029
1030 if let Some(err) = mkdirs_error {
1031 if attempt == 0 {
1032 continue;
1033 }
1034 return Err(err);
1035 }
1036
1037 let unique_trash_path = self.ensure_unique_trash_path(trash_path.clone()).await?;
1038 match self
1039 .rename_internal(&src_abs, &unique_trash_path, false, true)
1040 .await
1041 {
1042 Ok(()) => return Ok(Some(unique_trash_path)),
1043 Err(_) if attempt == 0 => continue,
1044 Err(err) => return Err(err),
1045 }
1046 }
1047
1048 Err(HdfsError::OperationFailed(
1049 "Failed to move to trash after retry".to_string(),
1050 ))
1051 }
1052
1053 pub async fn set_times(&self, path: &str, mtime: u64, atime: u64) -> Result<()> {
1055 let (link, resolved_path) = self.mount_table.resolve(path);
1056 link.protocol
1057 .set_times(&resolved_path, mtime, atime)
1058 .await?;
1059 Ok(())
1060 }
1061
1062 pub async fn set_owner(
1064 &self,
1065 path: &str,
1066 owner: Option<&str>,
1067 group: Option<&str>,
1068 ) -> Result<()> {
1069 let (link, resolved_path) = self.mount_table.resolve(path);
1070 link.protocol
1071 .set_owner(&resolved_path, owner, group)
1072 .await?;
1073 Ok(())
1074 }
1075
1076 pub async fn set_permission(&self, path: &str, permission: u32) -> Result<()> {
1081 let (link, resolved_path) = self.mount_table.resolve(path);
1082 link.protocol
1083 .set_permission(&resolved_path, permission)
1084 .await?;
1085 Ok(())
1086 }
1087
1088 pub async fn set_replication(&self, path: &str, replication: u32) -> Result<bool> {
1090 let (link, resolved_path) = self.mount_table.resolve(path);
1091 let result = link
1092 .protocol
1093 .set_replication(&resolved_path, replication)
1094 .await?
1095 .result;
1096
1097 Ok(result)
1098 }
1099
1100 pub async fn get_content_summary(&self, path: &str) -> Result<ContentSummary> {
1102 let (link, resolved_path) = self.mount_table.resolve(path);
1103 let result = link
1104 .protocol
1105 .get_content_summary(&resolved_path)
1106 .await?
1107 .summary;
1108
1109 Ok(result.into())
1110 }
1111
1112 pub async fn modify_acl_entries(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1114 let (link, resolved_path) = self.mount_table.resolve(path);
1115 link.protocol
1116 .modify_acl_entries(&resolved_path, acl_spec)
1117 .await?;
1118
1119 Ok(())
1120 }
1121
1122 pub async fn remove_acl_entries(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1124 let (link, resolved_path) = self.mount_table.resolve(path);
1125 link.protocol
1126 .remove_acl_entries(&resolved_path, acl_spec)
1127 .await?;
1128
1129 Ok(())
1130 }
1131
1132 pub async fn remove_default_acl(&self, path: &str) -> Result<()> {
1134 let (link, resolved_path) = self.mount_table.resolve(path);
1135 link.protocol.remove_default_acl(&resolved_path).await?;
1136
1137 Ok(())
1138 }
1139
1140 pub async fn remove_acl(&self, path: &str) -> Result<()> {
1142 let (link, resolved_path) = self.mount_table.resolve(path);
1143 link.protocol.remove_acl(&resolved_path).await?;
1144
1145 Ok(())
1146 }
1147
1148 pub async fn set_acl(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1152 let (link, resolved_path) = self.mount_table.resolve(path);
1153 link.protocol.set_acl(&resolved_path, acl_spec).await?;
1154
1155 Ok(())
1156 }
1157
1158 pub async fn get_acl_status(&self, path: &str) -> Result<AclStatus> {
1160 let (link, resolved_path) = self.mount_table.resolve(path);
1161 Ok(link
1162 .protocol
1163 .get_acl_status(&resolved_path)
1164 .await?
1165 .result
1166 .into())
1167 }
1168
1169 pub async fn glob_status(&self, pattern: &str) -> Result<Vec<FileStatus>> {
1172 let flattened = expand_glob(pattern.to_string())?;
1174
1175 let mut results: Vec<FileStatus> = Vec::new();
1176
1177 for flat in flattened.into_iter() {
1178 if flat.is_empty() {
1181 continue;
1182 }
1183
1184 let components = get_path_components(&flat);
1185
1186 #[derive(Clone, Debug)]
1188 struct Candidate {
1189 path: String,
1190 status: Option<FileStatus>,
1191 }
1192
1193 let mut candidates: Vec<Candidate> = vec![Candidate {
1195 path: "/".to_string(),
1196 status: None,
1197 }];
1198
1199 for (idx, comp) in components.iter().enumerate() {
1200 if candidates.is_empty() {
1201 break;
1202 }
1203
1204 let is_last = idx == components.len() - 1;
1205
1206 let unescaped = unescape_component(comp);
1207 let glob_pat = GlobPattern::new(comp)?;
1208
1209 if !is_last && !glob_pat.has_wildcard() {
1210 for cand in candidates.iter_mut() {
1212 if !cand.path.ends_with('/') {
1213 cand.path.push('/');
1214 }
1215 cand.path.push_str(&unescaped);
1216 }
1218 continue;
1219 }
1220
1221 let mut new_candidates: Vec<Candidate> = Vec::new();
1222
1223 for cand in candidates.into_iter() {
1224 if glob_pat.has_wildcard() {
1225 let listing = match self.list_status(&cand.path, false).await {
1227 Ok(listing) => listing,
1228 Err(HdfsError::FileNotFound(_)) => continue,
1229 Err(e) => return Err(e),
1230 };
1231 if listing.len() == 1 && listing[0].path == cand.path {
1232 continue;
1234 }
1235
1236 for child in listing.into_iter() {
1237 if !is_last && !child.isdir {
1239 continue;
1240 }
1241
1242 let name = child
1245 .path
1246 .rsplit_once('/')
1247 .map(|(_, n)| n)
1248 .unwrap_or(child.path.as_str());
1249
1250 if glob_pat.matches(name) {
1251 new_candidates.push(Candidate {
1252 path: child.path.clone(),
1253 status: Some(child),
1254 });
1255 }
1256 }
1257 } else {
1258 let mut next_path = cand.path.clone();
1260 if !next_path.ends_with('/') {
1261 next_path.push('/');
1262 }
1263 next_path.push_str(&unescaped);
1264
1265 match self.get_file_info(&next_path).await {
1266 Ok(status) => {
1267 if is_last || status.isdir {
1268 new_candidates.push(Candidate {
1269 path: status.path.clone(),
1270 status: Some(status),
1271 });
1272 }
1273 }
1274 Err(HdfsError::FileNotFound(_)) => continue,
1275 Err(e) => return Err(e),
1276 }
1277 }
1278 }
1279
1280 candidates = new_candidates;
1281 }
1282
1283 for cand in candidates.into_iter() {
1285 let status = if let Some(s) = cand.status {
1286 s
1287 } else {
1288 match self.get_file_info(&cand.path).await {
1290 Ok(s) => s,
1291 Err(HdfsError::FileNotFound(_)) => continue,
1292 Err(e) => return Err(e),
1293 }
1294 };
1295
1296 results.push(status);
1297 }
1298 }
1299
1300 Ok(results)
1301 }
1302}
1303
1304impl Default for Client {
1305 fn default() -> Self {
1308 ClientBuilder::new()
1309 .build()
1310 .expect("Failed to create default client")
1311 }
1312}
1313
1314pub(crate) struct DirListingIterator {
1315 path: String,
1316 resolved_path: String,
1317 link: MountLink,
1318 files_only: bool,
1319 partial_listing: VecDeque<HdfsFileStatusProto>,
1320 remaining: u32,
1321 last_seen: Vec<u8>,
1322}
1323
1324impl DirListingIterator {
1325 fn new(path: String, mount_table: &Arc<MountTable>, files_only: bool) -> Self {
1326 let (link, resolved_path) = mount_table.resolve(&path);
1327
1328 DirListingIterator {
1329 path,
1330 resolved_path,
1331 link: link.clone(),
1332 files_only,
1333 partial_listing: VecDeque::new(),
1334 remaining: 1,
1335 last_seen: Vec::new(),
1336 }
1337 }
1338
1339 async fn get_next_batch(&mut self) -> Result<bool> {
1340 let listing = self
1341 .link
1342 .protocol
1343 .get_listing(&self.resolved_path, self.last_seen.clone(), false)
1344 .await?;
1345
1346 if let Some(dir_list) = listing.dir_list {
1347 self.last_seen = dir_list
1348 .partial_listing
1349 .last()
1350 .map(|p| p.path.clone())
1351 .unwrap_or(Vec::new());
1352
1353 self.remaining = dir_list.remaining_entries;
1354
1355 self.partial_listing = dir_list
1356 .partial_listing
1357 .into_iter()
1358 .filter(|s| !self.files_only || s.file_type() != FileType::IsDir)
1359 .collect();
1360 Ok(!self.partial_listing.is_empty())
1361 } else {
1362 Err(HdfsError::FileNotFound(self.path.clone()))
1363 }
1364 }
1365
1366 pub async fn next(&mut self) -> Option<Result<FileStatus>> {
1367 if self.partial_listing.is_empty()
1368 && self.remaining > 0
1369 && let Err(error) = self.get_next_batch().await
1370 {
1371 self.remaining = 0;
1372 return Some(Err(error));
1373 }
1374 if let Some(next) = self.partial_listing.pop_front() {
1375 Some(Ok(FileStatus::from(next, &self.path)))
1376 } else {
1377 None
1378 }
1379 }
1380}
1381
1382pub struct ListStatusIterator {
1383 mount_table: Arc<MountTable>,
1384 recursive: bool,
1385 iters: Arc<tokio::sync::Mutex<Vec<DirListingIterator>>>,
1386}
1387
1388impl ListStatusIterator {
1389 fn new(path: String, mount_table: Arc<MountTable>, recursive: bool) -> Self {
1390 let initial = DirListingIterator::new(path.clone(), &mount_table, false);
1391
1392 ListStatusIterator {
1393 mount_table,
1394 recursive,
1395 iters: Arc::new(tokio::sync::Mutex::new(vec![initial])),
1396 }
1397 }
1398
1399 pub async fn next(&self) -> Option<Result<FileStatus>> {
1400 let mut next_file: Option<Result<FileStatus>> = None;
1401 let mut iters = self.iters.lock().await;
1402 while next_file.is_none() {
1403 if let Some(iter) = iters.last_mut() {
1404 if let Some(file_result) = iter.next().await {
1405 if let Ok(file) = file_result {
1406 if file.isdir && self.recursive {
1409 iters.push(DirListingIterator::new(
1410 file.path.clone(),
1411 &self.mount_table,
1412 false,
1413 ))
1414 }
1415 next_file = Some(Ok(file));
1416 } else {
1417 next_file = Some(file_result)
1419 }
1420 } else {
1421 iters.pop();
1423 }
1424 } else {
1425 break;
1427 }
1428 }
1429
1430 next_file
1431 }
1432
1433 pub fn into_stream(self) -> BoxStream<'static, Result<FileStatus>> {
1434 let listing = stream::unfold(self, |state| async move {
1435 let next = state.next().await;
1436 next.map(|n| (n, state))
1437 });
1438 Box::pin(listing)
1439 }
1440}
1441
1442#[derive(Debug, Clone)]
1443pub struct FileStatus {
1444 pub path: String,
1445 pub length: usize,
1446 pub isdir: bool,
1447 pub permission: u16,
1448 pub owner: String,
1449 pub group: String,
1450 pub modification_time: u64,
1451 pub access_time: u64,
1452 pub replication: Option<u32>,
1453 pub blocksize: Option<u64>,
1454}
1455
1456impl FileStatus {
1457 fn from(value: HdfsFileStatusProto, base_path: &str) -> Self {
1458 let mut path = base_path.trim_end_matches("/").to_string();
1459 let relative_path = std::str::from_utf8(&value.path).unwrap();
1460 if !relative_path.is_empty() {
1461 path.push('/');
1462 path.push_str(relative_path);
1463 }
1464
1465 if path.is_empty() {
1467 path.push('/');
1468 }
1469
1470 FileStatus {
1471 isdir: value.file_type() == FileType::IsDir,
1472 path,
1473 length: value.length as usize,
1474 permission: value.permission.perm as u16,
1475 owner: value.owner,
1476 group: value.group,
1477 modification_time: value.modification_time,
1478 access_time: value.access_time,
1479 replication: value.block_replication,
1480 blocksize: value.blocksize,
1481 }
1482 }
1483}
1484
1485#[derive(Debug)]
1486pub struct ContentSummary {
1487 pub length: u64,
1488 pub file_count: u64,
1489 pub directory_count: u64,
1490 pub quota: u64,
1491 pub space_consumed: u64,
1492 pub space_quota: u64,
1493}
1494
1495impl From<ContentSummaryProto> for ContentSummary {
1496 fn from(value: ContentSummaryProto) -> Self {
1497 ContentSummary {
1498 length: value.length,
1499 file_count: value.file_count,
1500 directory_count: value.directory_count,
1501 quota: value.quota,
1502 space_consumed: value.space_consumed,
1503 space_quota: value.space_quota,
1504 }
1505 }
1506}
1507
1508#[cfg(test)]
1509mod test {
1510 use std::sync::{Arc, LazyLock};
1511
1512 use tokio::runtime::Runtime;
1513 use url::Url;
1514
1515 use crate::{
1516 client::ClientBuilder,
1517 common::config::Configuration,
1518 hdfs::{protocol::NamenodeProtocol, proxy::NameServiceProxy},
1519 };
1520
1521 use super::{MountLink, MountTable};
1522
1523 static RT: LazyLock<Runtime> = LazyLock::new(|| Runtime::new().unwrap());
1524
1525 fn create_protocol(url: &str) -> Arc<NamenodeProtocol> {
1526 let proxy = NameServiceProxy::new(
1527 &Url::parse(url).unwrap(),
1528 Arc::new(Configuration::new(None, None).unwrap()),
1529 RT.handle().clone(),
1530 None,
1531 None,
1532 )
1533 .unwrap();
1534 Arc::new(NamenodeProtocol::new(proxy, RT.handle().clone()))
1535 }
1536
1537 #[test]
1538 fn test_default_fs() {
1539 assert!(
1540 ClientBuilder::new()
1541 .with_config(vec![("fs.defaultFS", "hdfs://test:9000")])
1542 .build()
1543 .is_ok()
1544 );
1545
1546 assert!(
1547 ClientBuilder::new()
1548 .with_config(vec![("fs.defaultFS", "hdfs://")])
1549 .build()
1550 .is_err()
1551 );
1552
1553 assert!(
1554 ClientBuilder::new()
1555 .with_url("hdfs://")
1556 .with_config(vec![("fs.defaultFS", "hdfs://test:9000")])
1557 .build()
1558 .is_ok()
1559 );
1560
1561 assert!(
1562 ClientBuilder::new()
1563 .with_url("hdfs://")
1564 .with_config(vec![("fs.defaultFS", "hdfs://")])
1565 .build()
1566 .is_err()
1567 );
1568
1569 assert!(
1570 ClientBuilder::new()
1571 .with_url("hdfs://")
1572 .with_config(vec![("fs.defaultFS", "viewfs://test")])
1573 .build()
1574 .is_err()
1575 );
1576 }
1577
1578 #[test]
1579 fn test_mount_link_resolve() {
1580 let protocol = create_protocol("hdfs://127.0.0.1:9000");
1581 let link = MountLink::new("/view", "/hdfs", protocol);
1582
1583 assert_eq!(link.resolve("/view/dir/file").unwrap(), "/hdfs/dir/file");
1584 assert_eq!(link.resolve("/view").unwrap(), "/hdfs");
1585 assert!(link.resolve("/hdfs/path").is_none());
1586 }
1587
1588 #[test]
1589 fn test_fallback_link() {
1590 let protocol = create_protocol("hdfs://127.0.0.1:9000");
1591 let link = MountLink::new("", "/hdfs", Arc::clone(&protocol));
1592
1593 assert_eq!(link.resolve("/path/to/file").unwrap(), "/hdfs/path/to/file");
1594 assert_eq!(link.resolve("/").unwrap(), "/hdfs/");
1595 assert_eq!(link.resolve("/hdfs/path").unwrap(), "/hdfs/hdfs/path");
1596
1597 let link = MountLink::new("", "", protocol);
1598 assert_eq!(link.resolve("/").unwrap(), "/");
1599 }
1600
1601 #[test]
1602 fn test_mount_table_resolve() {
1603 let link1 = MountLink::new(
1604 "/mount1",
1605 "/path1/nested",
1606 create_protocol("hdfs://127.0.0.1:9000"),
1607 );
1608 let link2 = MountLink::new(
1609 "/mount2",
1610 "/path2",
1611 create_protocol("hdfs://127.0.0.1:9001"),
1612 );
1613 let link3 = MountLink::new(
1614 "/mount3/nested",
1615 "/path3",
1616 create_protocol("hdfs://127.0.0.1:9002"),
1617 );
1618 let fallback = MountLink::new("/", "/path4", create_protocol("hdfs://127.0.0.1:9003"));
1619
1620 let mount_table = MountTable {
1621 mounts: vec![link1, link2, link3],
1622 fallback,
1623 home_dir: "/user/test".to_string(),
1624 };
1625
1626 let (link, resolved) = mount_table.resolve("/mount1");
1628 assert_eq!(link.viewfs_path, "/mount1");
1629 assert_eq!(resolved, "/path1/nested");
1630
1631 let (link, resolved) = mount_table.resolve("/mount1/");
1633 assert_eq!(link.viewfs_path, "/mount1");
1634 assert_eq!(resolved, "/path1/nested/");
1635
1636 let (link, resolved) = mount_table.resolve("/mount12");
1638 assert_eq!(link.viewfs_path, "");
1639 assert_eq!(resolved, "/path4/mount12");
1640
1641 let (link, resolved) = mount_table.resolve("/mount3/file");
1642 assert_eq!(link.viewfs_path, "");
1643 assert_eq!(resolved, "/path4/mount3/file");
1644
1645 let (link, resolved) = mount_table.resolve("/mount3/nested/file");
1646 assert_eq!(link.viewfs_path, "/mount3/nested");
1647 assert_eq!(resolved, "/path3/file");
1648
1649 let (link, resolved) = mount_table.resolve("file");
1650 assert_eq!(link.viewfs_path, "");
1651 assert_eq!(resolved, "/path4/user/test/file");
1652
1653 let (link, resolved) = mount_table.resolve("dir/subdir");
1654 assert_eq!(link.viewfs_path, "");
1655 assert_eq!(resolved, "/path4/user/test/dir/subdir");
1656
1657 let mount_table = MountTable {
1658 mounts: vec![
1659 MountLink::new(
1660 "/mount1",
1661 "/path1/nested",
1662 create_protocol("hdfs://127.0.0.1:9000"),
1663 ),
1664 MountLink::new(
1665 "/mount2",
1666 "/path2",
1667 create_protocol("hdfs://127.0.0.1:9001"),
1668 ),
1669 ],
1670 fallback: MountLink::new("/", "/path4", create_protocol("hdfs://127.0.0.1:9003")),
1671 home_dir: "/mount1/user".to_string(),
1672 };
1673
1674 let (link, resolved) = mount_table.resolve("file");
1675 assert_eq!(link.viewfs_path, "/mount1");
1676 assert_eq!(resolved, "/path1/nested/user/file");
1677
1678 let (link, resolved) = mount_table.resolve("dir/subdir");
1679 assert_eq!(link.viewfs_path, "/mount1");
1680 assert_eq!(resolved, "/path1/nested/user/dir/subdir");
1681 }
1682
1683 #[test]
1684 fn test_io_runtime() {
1685 assert!(
1686 ClientBuilder::new()
1687 .with_url("hdfs://127.0.0.1:9000")
1688 .with_io_runtime(Runtime::new().unwrap())
1689 .build()
1690 .is_ok()
1691 );
1692
1693 let rt = Runtime::new().unwrap();
1694 assert!(
1695 ClientBuilder::new()
1696 .with_url("hdfs://127.0.0.1:9000")
1697 .with_io_runtime(rt.handle().clone())
1698 .build()
1699 .is_ok()
1700 );
1701 }
1702
1703 #[test]
1704 fn test_with_user_sets_relative_path_home_dir() {
1705 let client = ClientBuilder::new()
1706 .with_url("hdfs://127.0.0.1:9000")
1707 .with_user("alice")
1708 .build()
1709 .unwrap();
1710
1711 let (_, resolved) = client.mount_table.resolve("file");
1712 assert_eq!(resolved, "/user/alice/file");
1713 }
1714
1715 #[test]
1716 fn test_kerberos_credentials_set_principal_home_dir() {
1717 let client = ClientBuilder::new()
1718 .with_url("hdfs://127.0.0.1:9000")
1719 .with_config([("hadoop.security.authentication", "kerberos")])
1720 .with_kerberos_principal("alice@EXAMPLE.COM")
1721 .with_kerberos_cache("FILE:/run/krb5/alice.ccache")
1722 .build()
1723 .unwrap();
1724
1725 let (_, resolved) = client.mount_table.resolve("file");
1726 assert_eq!(resolved, "/user/alice/file");
1727 }
1728
1729 #[test]
1730 fn test_explicit_kerberos_credentials_require_principal() {
1731 let error = ClientBuilder::new()
1732 .with_url("hdfs://127.0.0.1:9000")
1733 .with_kerberos_keytab("client.keytab")
1734 .build()
1735 .unwrap_err();
1736
1737 assert!(error.to_string().contains("principal is required"));
1738 }
1739
1740 #[test]
1741 fn test_set_conf_dir() {
1742 assert!(
1743 ClientBuilder::new()
1744 .with_url("hdfs://127.0.0.1:9000")
1745 .with_config_dir("target/test")
1746 .build()
1747 .is_ok()
1748 )
1749 }
1750}