1use std::borrow::Cow;
2use std::collections::VecDeque;
3use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5
6use polars_buffer::Buffer;
7use polars_core::config;
8use polars_core::error::{PolarsResult, polars_bail, to_compute_err};
9use polars_core::runtime::ASYNC;
10use polars_utils::pl_path::{CloudScheme, PlRefPath};
11use polars_utils::pl_str::PlSmallStr;
12
13#[cfg(feature = "cloud")]
14mod hugging_face;
15
16use crate::cloud::CloudOptions;
17
18#[allow(clippy::bind_instead_of_map)]
19pub static POLARS_TEMP_DIR_BASE_PATH: LazyLock<Box<Path>> = LazyLock::new(|| {
20 (|| {
21 let verbose = config::verbose();
22
23 let path = if let Ok(v) = std::env::var("POLARS_TEMP_DIR").map(PathBuf::from) {
24 if verbose {
25 eprintln!("init_temp_dir: sourced from POLARS_TEMP_DIR")
26 }
27 v
28 } else if cfg!(target_family = "unix") {
29 let id = std::env::var("USER")
30 .inspect(|_| {
31 if verbose {
32 eprintln!("init_temp_dir: sourced $USER")
33 }
34 })
35 .or_else(|_e| {
36 #[cfg(feature = "file_cache")]
39 {
40 std::env::var("HOME")
41 .inspect(|_| {
42 if verbose {
43 eprintln!("init_temp_dir: sourced $HOME")
44 }
45 })
46 .map(|x| blake3::hash(x.as_bytes()).to_hex()[..32].to_string())
47 }
48 #[cfg(not(feature = "file_cache"))]
49 {
50 Err(_e)
51 }
52 });
53
54 if let Ok(v) = id {
55 std::env::temp_dir().join(format!("polars-{v}/"))
56 } else {
57 return Err(std::io::Error::other(
58 "could not load $USER or $HOME environment variables",
59 ));
60 }
61 } else if cfg!(target_family = "windows") {
62 std::env::temp_dir().join("polars/")
66 } else {
67 std::env::temp_dir().join("polars/")
68 }
69 .into_boxed_path();
70
71 let perm_result = create_dir_owner_only(path.as_ref());
72
73 if std::env::var("POLARS_ALLOW_UNSECURED_TEMP_DIR").as_deref() != Ok("1") {
74 perm_result?;
75 }
76
77 std::io::Result::Ok(path)
78 })()
79 .map_err(|e| {
80 std::io::Error::new(
81 e.kind(),
82 format!(
83 "error initializing temporary directory: {e} \
84 consider explicitly setting POLARS_TEMP_DIR"
85 ),
86 )
87 })
88 .unwrap()
89});
90
91pub fn create_dir_owner_only(path: &Path) -> std::io::Result<()> {
93 std::fs::create_dir_all(path)?;
94
95 #[cfg(target_family = "unix")]
96 {
97 use std::os::unix::fs::PermissionsExt;
98
99 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
100 let perms = std::fs::metadata(path)?.permissions();
101
102 if (perms.mode() % 0o1000) != 0o700 {
103 return Err(std::io::Error::other(format!(
104 "error setting directory permissions: permission mismatch: {perms:?} (path = {path:?})"
105 )));
106 }
107 }
108
109 Ok(())
110}
111
112pub fn resolve_homedir<'a, S: AsRef<Path> + ?Sized>(path: &'a S) -> Cow<'a, Path> {
114 return inner(path.as_ref());
115
116 fn inner(path: &Path) -> Cow<'_, Path> {
117 if path.starts_with("~") {
118 #[cfg(not(target_family = "wasm"))]
120 if let Some(homedir) = home::home_dir() {
121 return Cow::Owned(homedir.join(path.strip_prefix("~").unwrap()));
122 }
123 }
124
125 Cow::Borrowed(path)
126 }
127}
128
129fn has_glob(path: &[u8]) -> bool {
130 return get_glob_start_idx(path).is_some();
131
132 fn get_glob_start_idx(path: &[u8]) -> Option<usize> {
134 memchr::memchr3(b'*', b'?', b'[', path)
135 }
136}
137
138pub fn expanded_from_single_directory(paths: &[PlRefPath], expanded_paths: &[PlRefPath]) -> bool {
140 paths.len() == 1 && !has_glob(paths[0].strip_scheme().as_bytes())
142 && {
144 (
145 !paths[0].has_scheme() && paths[0].as_std_path().is_dir()
147 )
148 || (
149 expanded_paths.is_empty() || (paths[0] != expanded_paths[0])
152 )
153 }
154}
155
156pub async fn expand_paths(
158 paths: &[PlRefPath],
159 glob: bool,
160 hidden_file_prefix: &[PlSmallStr],
161 #[allow(unused_variables)] cloud_options: &mut Option<CloudOptions>,
162) -> PolarsResult<Buffer<PlRefPath>> {
163 expand_paths_hive(paths, glob, hidden_file_prefix, cloud_options, false)
164 .await
165 .map(|x| x.0)
166}
167
168struct HiveIdxTracker<'a> {
169 idx: usize,
170 paths: &'a [PlRefPath],
171 check_directory_level: bool,
172}
173
174impl HiveIdxTracker<'_> {
175 fn update(&mut self, i: usize, path_idx: usize) -> PolarsResult<()> {
176 let check_directory_level = self.check_directory_level;
177 let paths = self.paths;
178
179 if check_directory_level
180 && ![usize::MAX, i].contains(&self.idx)
181 && (path_idx > 0 && paths[path_idx].parent() != paths[path_idx - 1].parent())
183 {
184 polars_bail!(
185 InvalidOperation:
186 "attempted to read from different directory levels with hive partitioning enabled: \
187 first path: {}, second path: {}",
188 &paths[path_idx - 1],
189 &paths[path_idx],
190 )
191 } else {
192 self.idx = std::cmp::min(self.idx, i);
193 Ok(())
194 }
195 }
196}
197
198#[cfg(feature = "cloud")]
199async fn expand_path_cloud(
200 path: PlRefPath,
201 cloud_options: Option<&CloudOptions>,
202 glob: bool,
203 first_path_has_scheme: bool,
204) -> PolarsResult<(usize, Vec<PlRefPath>)> {
205 let format_path = |scheme: &str, bucket: &str, location: &str| {
206 if first_path_has_scheme {
207 format!("{scheme}://{bucket}/{location}")
208 } else {
209 format!("/{location}")
210 }
211 };
212
213 use polars_utils::_limit_path_len_io_err;
214
215 use crate::cloud::object_path_from_str;
216 let path_str = path.as_str();
217
218 let (cloud_location, store) =
219 crate::cloud::build_object_store(path.clone(), cloud_options, glob).await?;
220 let prefix = object_path_from_str(&cloud_location.prefix)?;
221
222 let out = if !path_str.ends_with("/") && (!glob || cloud_location.expansion.is_none()) && {
223 path.has_scheme() || path.as_std_path().is_file()
228 } {
229 (
230 0,
231 vec![PlRefPath::new(format_path(
232 cloud_location.scheme,
233 &cloud_location.bucket,
234 prefix.as_ref(),
235 ))],
236 )
237 } else {
238 use futures::TryStreamExt;
239
240 if !path.has_scheme() {
241 path.as_std_path()
246 .metadata()
247 .map_err(|err| _limit_path_len_io_err(path.as_std_path(), err))?;
248 }
249
250 let cloud_location = &cloud_location;
251 let prefix_ref = &prefix;
252
253 let mut paths = store
254 .exec_with_rebuild_retry_on_err(|s| async move {
255 let out = s
256 .list(Some(prefix_ref))
257 .try_filter_map(|x| async move {
258 let out = (x.size > 0).then(|| {
259 PlRefPath::new({
260 format_path(
261 cloud_location.scheme,
262 &cloud_location.bucket,
263 x.location.as_ref(),
264 )
265 })
266 });
267 Ok(out)
268 })
269 .try_collect::<Vec<_>>()
270 .await?;
271
272 Ok(out)
273 })
274 .await?;
275
276 let mut prefix = prefix.to_string();
279 if path_str.ends_with('/') && !prefix.ends_with('/') {
280 prefix.push('/')
281 };
282
283 paths.sort_unstable();
284
285 (
286 format_path(
287 cloud_location.scheme,
288 &cloud_location.bucket,
289 prefix.as_ref(),
290 )
291 .len(),
292 paths,
293 )
294 };
295
296 PolarsResult::Ok(out)
297}
298
299pub async fn expand_paths_hive(
303 paths: &[PlRefPath],
304 glob: bool,
305 hidden_file_prefix: &[PlSmallStr],
306 #[allow(unused_variables)] cloud_options: &mut Option<CloudOptions>,
307 check_directory_level: bool,
308) -> PolarsResult<(Buffer<PlRefPath>, usize)> {
309 let Some(first_path) = paths.first() else {
310 return Ok((vec![].into(), 0));
311 };
312
313 let first_path_has_scheme = first_path.has_scheme();
314
315 let is_hidden_file = move |path: &PlRefPath| {
316 path.file_name()
317 .and_then(|x| x.to_str())
318 .is_some_and(|file_name| {
319 hidden_file_prefix
320 .iter()
321 .any(|x| file_name.starts_with(x.as_str()))
322 })
323 };
324
325 let mut out_paths = OutPaths {
326 paths: vec![],
327 exts: [None, None],
328 is_hidden_file: &is_hidden_file,
329 };
330
331 let mut hive_idx_tracker = HiveIdxTracker {
332 idx: usize::MAX,
333 paths,
334 check_directory_level,
335 };
336
337 if first_path_has_scheme || {
338 cfg!(not(target_family = "windows")) && polars_config::config().force_async()
339 } {
340 #[cfg(feature = "cloud")]
341 {
342 if first_path.scheme() == Some(CloudScheme::Hf) {
343 let (expand_start_idx, paths) = hugging_face::expand_paths_hf(
344 paths,
345 check_directory_level,
346 cloud_options,
347 glob,
348 )
349 .await?;
350
351 return Ok((paths.into(), expand_start_idx));
352 }
353
354 for (path_idx, path) in paths.iter().enumerate() {
355 use std::borrow::Cow;
356
357 let mut path = Cow::Borrowed(path);
358
359 if matches!(path.scheme(), Some(CloudScheme::Http | CloudScheme::Https)) {
360 let mut rewrite_aws = false;
361
362 #[cfg(feature = "aws")]
363 if let Some(p) = (|| {
364 use crate::cloud::CloudConfig;
365
366 let after_scheme = path.strip_scheme();
369
370 let bucket_end = after_scheme.find(".s3.")?;
371 let offset = bucket_end + 4;
372 let region_end = offset + after_scheme[offset..].find(".amazonaws.com/")?;
374
375 if after_scheme[..region_end].contains('/') || after_scheme.contains('?') {
377 return None;
378 }
379
380 let bucket = &after_scheme[..bucket_end];
381 let region = &after_scheme[bucket_end + 4..region_end];
382 let key = &after_scheme[region_end + 15..];
383
384 if let CloudConfig::Aws(configs) = cloud_options
385 .get_or_insert_default()
386 .config
387 .get_or_insert_with(|| CloudConfig::Aws(Vec::with_capacity(1)))
388 {
389 use object_store::aws::AmazonS3ConfigKey;
390
391 if !matches!(configs.last(), Some((AmazonS3ConfigKey::Region, _))) {
392 configs.push((AmazonS3ConfigKey::Region, region.into()))
393 }
394 }
395
396 Some(format!("s3://{bucket}/{key}"))
397 })() {
398 path = Cow::Owned(PlRefPath::new(p));
399 rewrite_aws = true;
400 }
401
402 if !rewrite_aws {
403 out_paths.push(path.into_owned());
404 hive_idx_tracker.update(0, path_idx)?;
405 continue;
406 }
407 }
408
409 let sort_start_idx = out_paths.paths.len();
410
411 if glob && has_glob(path.as_bytes()) {
412 hive_idx_tracker.update(0, path_idx)?;
413
414 let iter = ASYNC.block_in_place_on(crate::async_glob(
415 path.into_owned(),
416 cloud_options.as_ref(),
417 ))?;
418
419 if first_path_has_scheme {
420 out_paths.extend(iter.into_iter().map(PlRefPath::new))
421 } else {
422 out_paths.extend(iter.iter().map(|x| &x[7..]).map(PlRefPath::new))
425 };
426 } else {
427 let (expand_start_idx, paths) = expand_path_cloud(
428 path.into_owned(),
429 cloud_options.as_ref(),
430 glob,
431 first_path_has_scheme,
432 )
433 .await?;
434 out_paths.extend_from_slice(&paths);
435 hive_idx_tracker.update(expand_start_idx, path_idx)?;
436 };
437
438 if let Some(mut_slice) = out_paths.paths.get_mut(sort_start_idx..) {
439 <[PlRefPath]>::sort_unstable(mut_slice);
440 }
441 }
442 }
443 #[cfg(not(feature = "cloud"))]
444 panic!("Feature `cloud` must be enabled to use globbing patterns with cloud urls.")
445 } else {
446 let mut stack: VecDeque<Cow<'_, Path>> = VecDeque::new();
447 let mut paths_scratch: Vec<PathBuf> = vec![];
448
449 for (path_idx, path) in paths.iter().enumerate() {
450 stack.clear();
451 let sort_start_idx = out_paths.paths.len();
452
453 if path.as_std_path().is_dir() {
454 let i = path.as_str().len();
455
456 hive_idx_tracker.update(i, path_idx)?;
457
458 stack.push_back(Cow::Borrowed(path.as_std_path()));
459
460 while let Some(dir) = stack.pop_front() {
461 let mut last_err = Ok(());
462
463 paths_scratch.clear();
464 paths_scratch.extend(std::fs::read_dir(dir)?.map_while(|x| {
465 match x.map(|x| x.path()) {
466 Ok(v) => Some(v),
467 Err(e) => {
468 last_err = Err(e);
469 None
470 },
471 }
472 }));
473
474 last_err?;
475
476 for path in paths_scratch.drain(..) {
477 let md = path.metadata()?;
478
479 if md.is_dir() {
480 stack.push_back(Cow::Owned(path));
481 } else if md.len() > 0 {
482 out_paths.push(PlRefPath::try_from_path(&path)?);
483 }
484 }
485 }
486 } else if glob && has_glob(path.as_bytes()) {
487 hive_idx_tracker.update(0, path_idx)?;
488
489 let Ok(paths) = glob::glob(path.as_str()) else {
490 polars_bail!(ComputeError: "invalid glob pattern given")
491 };
492
493 for path in paths {
494 let path = path.map_err(to_compute_err)?;
495 let md = path.metadata()?;
496 if !md.is_dir() && md.len() > 0 {
497 out_paths.push(PlRefPath::try_from_path(&path)?);
498 }
499 }
500 } else {
501 hive_idx_tracker.update(0, path_idx)?;
502 out_paths.push(path.clone());
503 };
504
505 if let Some(mut_slice) = out_paths.paths.get_mut(sort_start_idx..) {
506 <[PlRefPath]>::sort_unstable(mut_slice);
507 }
508 }
509 }
510
511 if expanded_from_single_directory(paths, out_paths.paths.as_slice()) {
512 if let [Some((_, p1)), Some((_, p2))] = out_paths.exts {
513 polars_bail!(
514 InvalidOperation: "directory contained paths with different file extensions: \
515 first path: {}, second path: {}. Please use a glob pattern to explicitly specify \
516 which files to read (e.g. 'dir/**/*', 'dir/**/*.parquet')",
517 &p1, &p2
518 )
519 }
520 }
521
522 return Ok((out_paths.paths.into(), hive_idx_tracker.idx));
523
524 struct OutPaths<'a, F: Fn(&PlRefPath) -> bool> {
527 paths: Vec<PlRefPath>,
528 exts: [Option<(PlSmallStr, PlRefPath)>; 2],
529 is_hidden_file: &'a F,
530 }
531
532 impl<F> OutPaths<'_, F>
533 where
534 F: Fn(&PlRefPath) -> bool,
535 {
536 fn push(&mut self, value: PlRefPath) {
537 if (self.is_hidden_file)(&value) {
538 return;
539 }
540
541 let exts = &mut self.exts;
542 Self::update_ext_status(exts, &value);
543
544 self.paths.push(value)
545 }
546
547 fn extend(&mut self, values: impl IntoIterator<Item = PlRefPath>) {
548 let exts = &mut self.exts;
549
550 self.paths.extend(
551 values
552 .into_iter()
553 .filter(|x| !(self.is_hidden_file)(x))
554 .inspect(|x| {
555 Self::update_ext_status(exts, x);
556 }),
557 )
558 }
559
560 fn extend_from_slice(&mut self, values: &[PlRefPath]) {
561 self.extend(values.iter().cloned())
562 }
563
564 fn update_ext_status(exts: &mut [Option<(PlSmallStr, PlRefPath)>; 2], value: &PlRefPath) {
565 let ext = value
566 .extension()
567 .map_or(PlSmallStr::EMPTY, PlSmallStr::from);
568
569 if exts[0].is_none() {
570 exts[0] = Some((ext, value.clone()));
571 } else if exts[1].is_none() && ext != exts[0].as_ref().unwrap().0 {
572 exts[1] = Some((ext, value.clone()));
573 }
574 }
575 }
576}
577
578#[cfg(feature = "file_cache")]
580pub(crate) fn ensure_directory_init(path: &Path) -> std::io::Result<()> {
581 let result = std::fs::create_dir_all(path);
582
583 if path.is_dir() { Ok(()) } else { result }
584}
585
586#[cfg(test)]
587mod tests {
588 use std::path::PathBuf;
589
590 use polars_core::runtime::ASYNC;
591 use polars_utils::pl_path::PlRefPath;
592
593 use super::resolve_homedir;
594
595 #[cfg(not(target_os = "windows"))]
596 #[test]
597 fn test_resolve_homedir() {
598 let paths: Vec<PathBuf> = vec![
599 "~/dir1/dir2/test.csv".into(),
600 "/abs/path/test.csv".into(),
601 "rel/path/test.csv".into(),
602 "/".into(),
603 "~".into(),
604 ];
605
606 let resolved: Vec<PathBuf> = paths
607 .iter()
608 .map(resolve_homedir)
609 .map(|x| x.into_owned())
610 .collect();
611
612 assert_eq!(resolved[0].file_name(), paths[0].file_name());
613 assert!(resolved[0].is_absolute());
614 assert_eq!(resolved[1], paths[1]);
615 assert_eq!(resolved[2], paths[2]);
616 assert_eq!(resolved[3], paths[3]);
617 assert!(resolved[4].is_absolute());
618 }
619
620 #[cfg(target_os = "windows")]
621 #[test]
622 fn test_resolve_homedir_windows() {
623 let paths: Vec<PathBuf> = vec![
624 r#"c:\Users\user1\test.csv"#.into(),
625 r#"~\user1\test.csv"#.into(),
626 "~".into(),
627 ];
628
629 let resolved: Vec<PathBuf> = paths
630 .iter()
631 .map(resolve_homedir)
632 .map(|x| x.into_owned())
633 .collect();
634
635 assert_eq!(resolved[0], paths[0]);
636 assert_eq!(resolved[1].file_name(), paths[1].file_name());
637 assert!(resolved[1].is_absolute());
638 assert!(resolved[2].is_absolute());
639 }
640
641 #[test]
642 fn test_http_path_with_query_parameters_is_not_expanded_as_glob() {
643 use super::expand_paths;
647
648 let path = "https://pola.rs/test.csv?token=bear";
649 let paths = &[PlRefPath::new(path)];
650 let out = ASYNC
651 .block_on(expand_paths(paths, true, &[], &mut None))
652 .unwrap();
653 assert_eq!(out.as_ref(), paths);
654 }
655}