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 recover_lease(&self, src: &str) -> Result<bool> {
921 let (link, resolved_path) = self.mount_table.resolve(src);
922 link.protocol
923 .recover_lease(&resolved_path)
924 .await
925 .map(|response| response.result)
926 }
927
928 pub async fn mkdirs(&self, path: &str, permission: u32, create_parent: bool) -> Result<()> {
936 let (link, resolved_path) = self.mount_table.resolve(path);
937 link.protocol
938 .mkdirs(&resolved_path, permission, create_parent)
939 .await
940 .map(|_| ())
941 }
942
943 async fn rename_internal(
944 &self,
945 src: &str,
946 dst: &str,
947 overwrite: bool,
948 move_to_trash: bool,
949 ) -> Result<()> {
950 let (src_link, src_resolved_path) = self.mount_table.resolve(src);
951 let (dst_link, dst_resolved_path) = self.mount_table.resolve(dst);
952 if src_link.viewfs_path == dst_link.viewfs_path {
953 src_link
954 .protocol
955 .rename(
956 &src_resolved_path,
957 &dst_resolved_path,
958 overwrite,
959 move_to_trash,
960 )
961 .await
962 .map(|_| ())
963 } else {
964 Err(HdfsError::InvalidArgument(
965 "Cannot rename across different name services".to_string(),
966 ))
967 }
968 }
969
970 pub async fn rename(&self, src: &str, dst: &str, overwrite: bool) -> Result<()> {
972 self.rename_internal(src, dst, overwrite, false).await
973 }
974
975 pub async fn delete(&self, path: &str, recursive: bool) -> Result<bool> {
978 let (link, resolved_path) = self.mount_table.resolve(path);
979 link.protocol
980 .delete(&resolved_path, recursive)
981 .await
982 .map(|r| r.result)
983 }
984
985 pub async fn trash(&self, path: &str) -> Result<Option<String>> {
989 if path.is_empty() {
990 return Err(HdfsError::InvalidPath("Empty path".to_string()));
991 }
992
993 let src_abs = self.absolute_path(path);
994 if !self.trash_enabled(&src_abs).await? {
995 return Err(HdfsError::TrashNotEnabled);
996 }
997
998 let trash_root = self.trash_root_path();
999
1000 if Self::is_prefix_path(&trash_root, &src_abs) {
1001 return Ok(None);
1002 }
1003 if Self::is_prefix_path(&src_abs, &trash_root) {
1004 return Err(HdfsError::InvalidArgument(
1005 "Cannot move to trash because it contains the trash".to_string(),
1006 ));
1007 }
1008
1009 let _ = self.get_file_info(&src_abs).await?;
1010
1011 let (src_parent, src_name) = Self::split_parent_name(&src_abs)?;
1012 let trash_current = Self::join_paths(&trash_root, TRASH_CURRENT_DIR);
1013 let src_parent_rel = src_parent.trim_start_matches('/');
1014 let mut base_trash_path = if src_parent_rel.is_empty() {
1015 trash_current.clone()
1016 } else {
1017 Self::join_paths(&trash_current, src_parent_rel)
1018 };
1019 let mut trash_path = Self::join_paths(&base_trash_path, &src_name);
1020
1021 for attempt in 0..2 {
1022 let mut mkdirs_error: Option<HdfsError> = None;
1023 loop {
1024 match self
1025 .mkdirs(&base_trash_path, TRASH_DIR_PERMISSION, true)
1026 .await
1027 {
1028 Ok(()) => break,
1029 Err(err) => {
1030 if let Some(ancestor) = self.non_dir_ancestor(&base_trash_path).await? {
1031 let timestamp = Self::current_time_millis();
1032 base_trash_path = base_trash_path.replacen(
1033 &ancestor,
1034 &format!("{ancestor}{timestamp}"),
1035 1,
1036 );
1037 trash_path = Self::join_paths(&base_trash_path, &src_name);
1038 continue;
1039 }
1040 mkdirs_error = Some(err);
1041 break;
1042 }
1043 }
1044 }
1045
1046 if let Some(err) = mkdirs_error {
1047 if attempt == 0 {
1048 continue;
1049 }
1050 return Err(err);
1051 }
1052
1053 let unique_trash_path = self.ensure_unique_trash_path(trash_path.clone()).await?;
1054 match self
1055 .rename_internal(&src_abs, &unique_trash_path, false, true)
1056 .await
1057 {
1058 Ok(()) => return Ok(Some(unique_trash_path)),
1059 Err(_) if attempt == 0 => continue,
1060 Err(err) => return Err(err),
1061 }
1062 }
1063
1064 Err(HdfsError::OperationFailed(
1065 "Failed to move to trash after retry".to_string(),
1066 ))
1067 }
1068
1069 pub async fn set_times(&self, path: &str, mtime: u64, atime: u64) -> Result<()> {
1071 let (link, resolved_path) = self.mount_table.resolve(path);
1072 link.protocol
1073 .set_times(&resolved_path, mtime, atime)
1074 .await?;
1075 Ok(())
1076 }
1077
1078 pub async fn set_owner(
1080 &self,
1081 path: &str,
1082 owner: Option<&str>,
1083 group: Option<&str>,
1084 ) -> Result<()> {
1085 let (link, resolved_path) = self.mount_table.resolve(path);
1086 link.protocol
1087 .set_owner(&resolved_path, owner, group)
1088 .await?;
1089 Ok(())
1090 }
1091
1092 pub async fn set_permission(&self, path: &str, permission: u32) -> Result<()> {
1097 let (link, resolved_path) = self.mount_table.resolve(path);
1098 link.protocol
1099 .set_permission(&resolved_path, permission)
1100 .await?;
1101 Ok(())
1102 }
1103
1104 pub async fn set_replication(&self, path: &str, replication: u32) -> Result<bool> {
1106 let (link, resolved_path) = self.mount_table.resolve(path);
1107 let result = link
1108 .protocol
1109 .set_replication(&resolved_path, replication)
1110 .await?
1111 .result;
1112
1113 Ok(result)
1114 }
1115
1116 pub async fn get_content_summary(&self, path: &str) -> Result<ContentSummary> {
1118 let (link, resolved_path) = self.mount_table.resolve(path);
1119 let result = link
1120 .protocol
1121 .get_content_summary(&resolved_path)
1122 .await?
1123 .summary;
1124
1125 Ok(result.into())
1126 }
1127
1128 pub async fn modify_acl_entries(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1130 let (link, resolved_path) = self.mount_table.resolve(path);
1131 link.protocol
1132 .modify_acl_entries(&resolved_path, acl_spec)
1133 .await?;
1134
1135 Ok(())
1136 }
1137
1138 pub async fn remove_acl_entries(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1140 let (link, resolved_path) = self.mount_table.resolve(path);
1141 link.protocol
1142 .remove_acl_entries(&resolved_path, acl_spec)
1143 .await?;
1144
1145 Ok(())
1146 }
1147
1148 pub async fn remove_default_acl(&self, path: &str) -> Result<()> {
1150 let (link, resolved_path) = self.mount_table.resolve(path);
1151 link.protocol.remove_default_acl(&resolved_path).await?;
1152
1153 Ok(())
1154 }
1155
1156 pub async fn remove_acl(&self, path: &str) -> Result<()> {
1158 let (link, resolved_path) = self.mount_table.resolve(path);
1159 link.protocol.remove_acl(&resolved_path).await?;
1160
1161 Ok(())
1162 }
1163
1164 pub async fn set_acl(&self, path: &str, acl_spec: Vec<AclEntry>) -> Result<()> {
1168 let (link, resolved_path) = self.mount_table.resolve(path);
1169 link.protocol.set_acl(&resolved_path, acl_spec).await?;
1170
1171 Ok(())
1172 }
1173
1174 pub async fn get_acl_status(&self, path: &str) -> Result<AclStatus> {
1176 let (link, resolved_path) = self.mount_table.resolve(path);
1177 Ok(link
1178 .protocol
1179 .get_acl_status(&resolved_path)
1180 .await?
1181 .result
1182 .into())
1183 }
1184
1185 pub async fn glob_status(&self, pattern: &str) -> Result<Vec<FileStatus>> {
1188 let flattened = expand_glob(pattern.to_string())?;
1190
1191 let mut results: Vec<FileStatus> = Vec::new();
1192
1193 for flat in flattened.into_iter() {
1194 if flat.is_empty() {
1197 continue;
1198 }
1199
1200 let components = get_path_components(&flat);
1201
1202 #[derive(Clone, Debug)]
1204 struct Candidate {
1205 path: String,
1206 status: Option<FileStatus>,
1207 }
1208
1209 let mut candidates: Vec<Candidate> = vec![Candidate {
1211 path: "/".to_string(),
1212 status: None,
1213 }];
1214
1215 for (idx, comp) in components.iter().enumerate() {
1216 if candidates.is_empty() {
1217 break;
1218 }
1219
1220 let is_last = idx == components.len() - 1;
1221
1222 let unescaped = unescape_component(comp);
1223 let glob_pat = GlobPattern::new(comp)?;
1224
1225 if !is_last && !glob_pat.has_wildcard() {
1226 for cand in candidates.iter_mut() {
1228 if !cand.path.ends_with('/') {
1229 cand.path.push('/');
1230 }
1231 cand.path.push_str(&unescaped);
1232 }
1234 continue;
1235 }
1236
1237 let mut new_candidates: Vec<Candidate> = Vec::new();
1238
1239 for cand in candidates.into_iter() {
1240 if glob_pat.has_wildcard() {
1241 let listing = match self.list_status(&cand.path, false).await {
1243 Ok(listing) => listing,
1244 Err(HdfsError::FileNotFound(_)) => continue,
1245 Err(e) => return Err(e),
1246 };
1247 if listing.len() == 1 && listing[0].path == cand.path {
1248 continue;
1250 }
1251
1252 for child in listing.into_iter() {
1253 if !is_last && !child.isdir {
1255 continue;
1256 }
1257
1258 let name = child
1261 .path
1262 .rsplit_once('/')
1263 .map(|(_, n)| n)
1264 .unwrap_or(child.path.as_str());
1265
1266 if glob_pat.matches(name) {
1267 new_candidates.push(Candidate {
1268 path: child.path.clone(),
1269 status: Some(child),
1270 });
1271 }
1272 }
1273 } else {
1274 let mut next_path = cand.path.clone();
1276 if !next_path.ends_with('/') {
1277 next_path.push('/');
1278 }
1279 next_path.push_str(&unescaped);
1280
1281 match self.get_file_info(&next_path).await {
1282 Ok(status) => {
1283 if is_last || status.isdir {
1284 new_candidates.push(Candidate {
1285 path: status.path.clone(),
1286 status: Some(status),
1287 });
1288 }
1289 }
1290 Err(HdfsError::FileNotFound(_)) => continue,
1291 Err(e) => return Err(e),
1292 }
1293 }
1294 }
1295
1296 candidates = new_candidates;
1297 }
1298
1299 for cand in candidates.into_iter() {
1301 let status = if let Some(s) = cand.status {
1302 s
1303 } else {
1304 match self.get_file_info(&cand.path).await {
1306 Ok(s) => s,
1307 Err(HdfsError::FileNotFound(_)) => continue,
1308 Err(e) => return Err(e),
1309 }
1310 };
1311
1312 results.push(status);
1313 }
1314 }
1315
1316 Ok(results)
1317 }
1318}
1319
1320impl Default for Client {
1321 fn default() -> Self {
1324 ClientBuilder::new()
1325 .build()
1326 .expect("Failed to create default client")
1327 }
1328}
1329
1330pub(crate) struct DirListingIterator {
1331 path: String,
1332 resolved_path: String,
1333 link: MountLink,
1334 files_only: bool,
1335 partial_listing: VecDeque<HdfsFileStatusProto>,
1336 remaining: u32,
1337 last_seen: Vec<u8>,
1338}
1339
1340impl DirListingIterator {
1341 fn new(path: String, mount_table: &Arc<MountTable>, files_only: bool) -> Self {
1342 let (link, resolved_path) = mount_table.resolve(&path);
1343
1344 DirListingIterator {
1345 path,
1346 resolved_path,
1347 link: link.clone(),
1348 files_only,
1349 partial_listing: VecDeque::new(),
1350 remaining: 1,
1351 last_seen: Vec::new(),
1352 }
1353 }
1354
1355 async fn get_next_batch(&mut self) -> Result<bool> {
1356 let listing = self
1357 .link
1358 .protocol
1359 .get_listing(&self.resolved_path, self.last_seen.clone(), false)
1360 .await?;
1361
1362 if let Some(dir_list) = listing.dir_list {
1363 self.last_seen = dir_list
1364 .partial_listing
1365 .last()
1366 .map(|p| p.path.clone())
1367 .unwrap_or(Vec::new());
1368
1369 self.remaining = dir_list.remaining_entries;
1370
1371 self.partial_listing = dir_list
1372 .partial_listing
1373 .into_iter()
1374 .filter(|s| !self.files_only || s.file_type() != FileType::IsDir)
1375 .collect();
1376 Ok(!self.partial_listing.is_empty())
1377 } else {
1378 Err(HdfsError::FileNotFound(self.path.clone()))
1379 }
1380 }
1381
1382 pub async fn next(&mut self) -> Option<Result<FileStatus>> {
1383 if self.partial_listing.is_empty()
1384 && self.remaining > 0
1385 && let Err(error) = self.get_next_batch().await
1386 {
1387 self.remaining = 0;
1388 return Some(Err(error));
1389 }
1390 if let Some(next) = self.partial_listing.pop_front() {
1391 Some(Ok(FileStatus::from(next, &self.path)))
1392 } else {
1393 None
1394 }
1395 }
1396}
1397
1398pub struct ListStatusIterator {
1399 mount_table: Arc<MountTable>,
1400 recursive: bool,
1401 iters: Arc<tokio::sync::Mutex<Vec<DirListingIterator>>>,
1402}
1403
1404impl ListStatusIterator {
1405 fn new(path: String, mount_table: Arc<MountTable>, recursive: bool) -> Self {
1406 let initial = DirListingIterator::new(path.clone(), &mount_table, false);
1407
1408 ListStatusIterator {
1409 mount_table,
1410 recursive,
1411 iters: Arc::new(tokio::sync::Mutex::new(vec![initial])),
1412 }
1413 }
1414
1415 pub async fn next(&self) -> Option<Result<FileStatus>> {
1416 let mut next_file: Option<Result<FileStatus>> = None;
1417 let mut iters = self.iters.lock().await;
1418 while next_file.is_none() {
1419 if let Some(iter) = iters.last_mut() {
1420 if let Some(file_result) = iter.next().await {
1421 if let Ok(file) = file_result {
1422 if file.isdir && self.recursive {
1425 iters.push(DirListingIterator::new(
1426 file.path.clone(),
1427 &self.mount_table,
1428 false,
1429 ))
1430 }
1431 next_file = Some(Ok(file));
1432 } else {
1433 next_file = Some(file_result)
1435 }
1436 } else {
1437 iters.pop();
1439 }
1440 } else {
1441 break;
1443 }
1444 }
1445
1446 next_file
1447 }
1448
1449 pub fn into_stream(self) -> BoxStream<'static, Result<FileStatus>> {
1450 let listing = stream::unfold(self, |state| async move {
1451 let next = state.next().await;
1452 next.map(|n| (n, state))
1453 });
1454 Box::pin(listing)
1455 }
1456}
1457
1458#[derive(Debug, Clone)]
1459pub struct FileStatus {
1460 pub path: String,
1461 pub length: usize,
1462 pub isdir: bool,
1463 pub permission: u16,
1464 pub owner: String,
1465 pub group: String,
1466 pub modification_time: u64,
1467 pub access_time: u64,
1468 pub replication: Option<u32>,
1469 pub blocksize: Option<u64>,
1470}
1471
1472impl FileStatus {
1473 fn from(value: HdfsFileStatusProto, base_path: &str) -> Self {
1474 let mut path = base_path.trim_end_matches("/").to_string();
1475 let relative_path = std::str::from_utf8(&value.path).unwrap();
1476 if !relative_path.is_empty() {
1477 path.push('/');
1478 path.push_str(relative_path);
1479 }
1480
1481 if path.is_empty() {
1483 path.push('/');
1484 }
1485
1486 FileStatus {
1487 isdir: value.file_type() == FileType::IsDir,
1488 path,
1489 length: value.length as usize,
1490 permission: value.permission.perm as u16,
1491 owner: value.owner,
1492 group: value.group,
1493 modification_time: value.modification_time,
1494 access_time: value.access_time,
1495 replication: value.block_replication,
1496 blocksize: value.blocksize,
1497 }
1498 }
1499}
1500
1501#[derive(Debug)]
1502pub struct ContentSummary {
1503 pub length: u64,
1504 pub file_count: u64,
1505 pub directory_count: u64,
1506 pub quota: u64,
1507 pub space_consumed: u64,
1508 pub space_quota: u64,
1509}
1510
1511impl From<ContentSummaryProto> for ContentSummary {
1512 fn from(value: ContentSummaryProto) -> Self {
1513 ContentSummary {
1514 length: value.length,
1515 file_count: value.file_count,
1516 directory_count: value.directory_count,
1517 quota: value.quota,
1518 space_consumed: value.space_consumed,
1519 space_quota: value.space_quota,
1520 }
1521 }
1522}
1523
1524#[cfg(test)]
1525mod test {
1526 use std::sync::{Arc, LazyLock};
1527
1528 use tokio::runtime::Runtime;
1529 use url::Url;
1530
1531 use crate::{
1532 client::ClientBuilder,
1533 common::config::Configuration,
1534 hdfs::{protocol::NamenodeProtocol, proxy::NameServiceProxy},
1535 };
1536
1537 use super::{MountLink, MountTable};
1538
1539 static RT: LazyLock<Runtime> = LazyLock::new(|| Runtime::new().unwrap());
1540
1541 fn create_protocol(url: &str) -> Arc<NamenodeProtocol> {
1542 let proxy = NameServiceProxy::new(
1543 &Url::parse(url).unwrap(),
1544 Arc::new(Configuration::new(None, None).unwrap()),
1545 RT.handle().clone(),
1546 None,
1547 None,
1548 )
1549 .unwrap();
1550 Arc::new(NamenodeProtocol::new(proxy, RT.handle().clone()))
1551 }
1552
1553 #[test]
1554 fn test_default_fs() {
1555 assert!(
1556 ClientBuilder::new()
1557 .with_config(vec![("fs.defaultFS", "hdfs://test:9000")])
1558 .build()
1559 .is_ok()
1560 );
1561
1562 assert!(
1563 ClientBuilder::new()
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", "hdfs://test:9000")])
1573 .build()
1574 .is_ok()
1575 );
1576
1577 assert!(
1578 ClientBuilder::new()
1579 .with_url("hdfs://")
1580 .with_config(vec![("fs.defaultFS", "hdfs://")])
1581 .build()
1582 .is_err()
1583 );
1584
1585 assert!(
1586 ClientBuilder::new()
1587 .with_url("hdfs://")
1588 .with_config(vec![("fs.defaultFS", "viewfs://test")])
1589 .build()
1590 .is_err()
1591 );
1592 }
1593
1594 #[test]
1595 fn test_mount_link_resolve() {
1596 let protocol = create_protocol("hdfs://127.0.0.1:9000");
1597 let link = MountLink::new("/view", "/hdfs", protocol);
1598
1599 assert_eq!(link.resolve("/view/dir/file").unwrap(), "/hdfs/dir/file");
1600 assert_eq!(link.resolve("/view").unwrap(), "/hdfs");
1601 assert!(link.resolve("/hdfs/path").is_none());
1602 }
1603
1604 #[test]
1605 fn test_fallback_link() {
1606 let protocol = create_protocol("hdfs://127.0.0.1:9000");
1607 let link = MountLink::new("", "/hdfs", Arc::clone(&protocol));
1608
1609 assert_eq!(link.resolve("/path/to/file").unwrap(), "/hdfs/path/to/file");
1610 assert_eq!(link.resolve("/").unwrap(), "/hdfs/");
1611 assert_eq!(link.resolve("/hdfs/path").unwrap(), "/hdfs/hdfs/path");
1612
1613 let link = MountLink::new("", "", protocol);
1614 assert_eq!(link.resolve("/").unwrap(), "/");
1615 }
1616
1617 #[test]
1618 fn test_mount_table_resolve() {
1619 let link1 = MountLink::new(
1620 "/mount1",
1621 "/path1/nested",
1622 create_protocol("hdfs://127.0.0.1:9000"),
1623 );
1624 let link2 = MountLink::new(
1625 "/mount2",
1626 "/path2",
1627 create_protocol("hdfs://127.0.0.1:9001"),
1628 );
1629 let link3 = MountLink::new(
1630 "/mount3/nested",
1631 "/path3",
1632 create_protocol("hdfs://127.0.0.1:9002"),
1633 );
1634 let fallback = MountLink::new("/", "/path4", create_protocol("hdfs://127.0.0.1:9003"));
1635
1636 let mount_table = MountTable {
1637 mounts: vec![link1, link2, link3],
1638 fallback,
1639 home_dir: "/user/test".to_string(),
1640 };
1641
1642 let (link, resolved) = mount_table.resolve("/mount1");
1644 assert_eq!(link.viewfs_path, "/mount1");
1645 assert_eq!(resolved, "/path1/nested");
1646
1647 let (link, resolved) = mount_table.resolve("/mount1/");
1649 assert_eq!(link.viewfs_path, "/mount1");
1650 assert_eq!(resolved, "/path1/nested/");
1651
1652 let (link, resolved) = mount_table.resolve("/mount12");
1654 assert_eq!(link.viewfs_path, "");
1655 assert_eq!(resolved, "/path4/mount12");
1656
1657 let (link, resolved) = mount_table.resolve("/mount3/file");
1658 assert_eq!(link.viewfs_path, "");
1659 assert_eq!(resolved, "/path4/mount3/file");
1660
1661 let (link, resolved) = mount_table.resolve("/mount3/nested/file");
1662 assert_eq!(link.viewfs_path, "/mount3/nested");
1663 assert_eq!(resolved, "/path3/file");
1664
1665 let (link, resolved) = mount_table.resolve("file");
1666 assert_eq!(link.viewfs_path, "");
1667 assert_eq!(resolved, "/path4/user/test/file");
1668
1669 let (link, resolved) = mount_table.resolve("dir/subdir");
1670 assert_eq!(link.viewfs_path, "");
1671 assert_eq!(resolved, "/path4/user/test/dir/subdir");
1672
1673 let mount_table = MountTable {
1674 mounts: vec![
1675 MountLink::new(
1676 "/mount1",
1677 "/path1/nested",
1678 create_protocol("hdfs://127.0.0.1:9000"),
1679 ),
1680 MountLink::new(
1681 "/mount2",
1682 "/path2",
1683 create_protocol("hdfs://127.0.0.1:9001"),
1684 ),
1685 ],
1686 fallback: MountLink::new("/", "/path4", create_protocol("hdfs://127.0.0.1:9003")),
1687 home_dir: "/mount1/user".to_string(),
1688 };
1689
1690 let (link, resolved) = mount_table.resolve("file");
1691 assert_eq!(link.viewfs_path, "/mount1");
1692 assert_eq!(resolved, "/path1/nested/user/file");
1693
1694 let (link, resolved) = mount_table.resolve("dir/subdir");
1695 assert_eq!(link.viewfs_path, "/mount1");
1696 assert_eq!(resolved, "/path1/nested/user/dir/subdir");
1697 }
1698
1699 #[test]
1700 fn test_io_runtime() {
1701 assert!(
1702 ClientBuilder::new()
1703 .with_url("hdfs://127.0.0.1:9000")
1704 .with_io_runtime(Runtime::new().unwrap())
1705 .build()
1706 .is_ok()
1707 );
1708
1709 let rt = Runtime::new().unwrap();
1710 assert!(
1711 ClientBuilder::new()
1712 .with_url("hdfs://127.0.0.1:9000")
1713 .with_io_runtime(rt.handle().clone())
1714 .build()
1715 .is_ok()
1716 );
1717 }
1718
1719 #[test]
1720 fn test_with_user_sets_relative_path_home_dir() {
1721 let client = ClientBuilder::new()
1722 .with_url("hdfs://127.0.0.1:9000")
1723 .with_user("alice")
1724 .build()
1725 .unwrap();
1726
1727 let (_, resolved) = client.mount_table.resolve("file");
1728 assert_eq!(resolved, "/user/alice/file");
1729 }
1730
1731 #[test]
1732 fn test_kerberos_credentials_set_principal_home_dir() {
1733 let client = ClientBuilder::new()
1734 .with_url("hdfs://127.0.0.1:9000")
1735 .with_config([("hadoop.security.authentication", "kerberos")])
1736 .with_kerberos_principal("alice@EXAMPLE.COM")
1737 .with_kerberos_cache("FILE:/run/krb5/alice.ccache")
1738 .build()
1739 .unwrap();
1740
1741 let (_, resolved) = client.mount_table.resolve("file");
1742 assert_eq!(resolved, "/user/alice/file");
1743 }
1744
1745 #[test]
1746 fn test_explicit_kerberos_credentials_require_principal() {
1747 let error = ClientBuilder::new()
1748 .with_url("hdfs://127.0.0.1:9000")
1749 .with_kerberos_keytab("client.keytab")
1750 .build()
1751 .unwrap_err();
1752
1753 assert!(error.to_string().contains("principal is required"));
1754 }
1755
1756 #[test]
1757 fn test_set_conf_dir() {
1758 assert!(
1759 ClientBuilder::new()
1760 .with_url("hdfs://127.0.0.1:9000")
1761 .with_config_dir("target/test")
1762 .build()
1763 .is_ok()
1764 )
1765 }
1766}