python-fsutil 0.16.0__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {python_fsutil-0.16.0/src/python_fsutil.egg-info → python_fsutil-0.17.0}/PKG-INFO +2 -1
  2. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/README.md +1 -0
  3. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/converters.py +1 -1
  4. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/info.py +21 -7
  5. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/metadata.py +1 -1
  6. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/operations.py +6 -2
  7. {python_fsutil-0.16.0 → python_fsutil-0.17.0/src/python_fsutil.egg-info}/PKG-INFO +2 -1
  8. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_converters.py +9 -0
  9. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_info.py +15 -0
  10. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_operations.py +45 -0
  11. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/LICENSE.txt +0 -0
  12. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/MANIFEST.in +0 -0
  13. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/pyproject.toml +0 -0
  14. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/setup.cfg +0 -0
  15. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/setup.py +0 -0
  16. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/__init__.py +0 -0
  17. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/archives.py +0 -0
  18. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/args.py +0 -0
  19. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/checks.py +0 -0
  20. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/deps.py +0 -0
  21. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/io.py +0 -0
  22. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/paths.py +0 -0
  23. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/perms.py +0 -0
  24. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/py.typed +0 -0
  25. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/fsutil/types.py +0 -0
  26. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/python_fsutil.egg-info/SOURCES.txt +0 -0
  27. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/python_fsutil.egg-info/dependency_links.txt +0 -0
  28. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/src/python_fsutil.egg-info/top_level.txt +0 -0
  29. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/__init__.py +0 -0
  30. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/conftest.py +0 -0
  31. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_archives.py +0 -0
  32. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_args.py +0 -0
  33. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_checks.py +0 -0
  34. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_deps.py +0 -0
  35. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_io.py +0 -0
  36. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_metadata.py +0 -0
  37. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_paths.py +0 -0
  38. {python_fsutil-0.16.0 → python_fsutil-0.17.0}/tests/test_perms.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fsutil
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: high-level file-system operations for lazy devs.
5
5
  Author-email: Fabio Caccamo <fabio.caccamo@gmail.com>
6
6
  Maintainer-email: Fabio Caccamo <fabio.caccamo@gmail.com>
@@ -69,6 +69,7 @@ Dynamic: license-file
69
69
  [![](https://img.shields.io/codacy/grade/fc40788ae7d74d1fb34a38934113c4e5?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/python-fsutil)
70
70
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
71
71
  [![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
72
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fabiocaccamo/python-fsutil/badge)](https://securityscorecards.dev/viewer/?uri=github.com/fabiocaccamo/python-fsutil)
72
73
 
73
74
  # python-fsutil
74
75
  high-level file-system operations for lazy devs.
@@ -10,6 +10,7 @@
10
10
  [![](https://img.shields.io/codacy/grade/fc40788ae7d74d1fb34a38934113c4e5?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/python-fsutil)
11
11
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
12
12
  [![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
13
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fabiocaccamo/python-fsutil/badge)](https://securityscorecards.dev/viewer/?uri=github.com/fabiocaccamo/python-fsutil)
13
14
 
14
15
  # python-fsutil
15
16
  high-level file-system operations for lazy devs.
@@ -11,7 +11,7 @@ def convert_size_bytes_to_string(size: int) -> str:
11
11
  units = SIZE_UNITS
12
12
  factor = 0
13
13
  factor_limit = len(units) - 1
14
- while (size_num >= 1024) and (factor <= factor_limit):
14
+ while (size_num >= 1024) and (factor < factor_limit):
15
15
  size_num /= 1024
16
16
  factor += 1
17
17
  size_units = units[factor]
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import os
5
+ import pathlib
5
6
  from datetime import datetime
7
+ from typing import IO
6
8
 
7
9
  from fsutil.args import get_path as _get_path
8
10
  from fsutil.checks import assert_dir, assert_file
@@ -129,17 +131,29 @@ def get_file_creation_date_formatted(
129
131
  return date.strftime(format)
130
132
 
131
133
 
132
- def get_file_hash(path: PathIn, *, func: str = "md5") -> str:
134
+ def get_file_hash(path: PathIn | IO[bytes], *, func: str = "md5") -> str:
133
135
  """
134
- Get the hash of the file at the given path using
135
- the specified algorithm function (md5 by default).
136
+ Get the hash of the file at the given path (or of the given
137
+ binary file-like object) using the specified algorithm
138
+ function (md5 by default).
136
139
  """
137
- path = _get_path(path)
138
- assert_file(path)
139
140
  hash = hashlib.new(func)
140
- with open(path, "rb") as file:
141
- for chunk in iter(lambda: file.read(4096), b""):
141
+ if isinstance(path, (str, pathlib.Path)):
142
+ path = _get_path(path)
143
+ assert_file(path)
144
+ with open(path, "rb") as file:
145
+ for chunk in iter(lambda: file.read(4096), b""):
146
+ hash.update(chunk)
147
+ else:
148
+ fileobj = path
149
+ position = None
150
+ if fileobj.seekable():
151
+ position = fileobj.tell()
152
+ fileobj.seek(0)
153
+ for chunk in iter(lambda: fileobj.read(4096), b""):
142
154
  hash.update(chunk)
155
+ if position is not None:
156
+ fileobj.seek(position)
143
157
  hash_hex = hash.hexdigest()
144
158
  return hash_hex
145
159
 
@@ -4,4 +4,4 @@ __description__ = "high-level file-system operations for lazy devs."
4
4
  __email__ = "fabio.caccamo@gmail.com"
5
5
  __license__ = "MIT"
6
6
  __title__ = "python-fsutil"
7
- __version__ = "0.16.0"
7
+ __version__ = "0.17.0"
@@ -213,7 +213,9 @@ def download_file(
213
213
  filename_pattern = r'filename="(.*)"'
214
214
  filename_match = re.search(filename_pattern, content_disposition)
215
215
  if filename_match:
216
- filename = filename_match.group(1)
216
+ # sanitize Content-Disposition filename to prevent path traversal
217
+ filename = filename_match.group(1).replace("\\", "/")
218
+ filename = os.path.basename(filename)
217
219
  # or detect filename from url
218
220
  if not filename:
219
221
  filename = get_filename(url)
@@ -518,7 +520,9 @@ def _search_paths(path: PathIn, pattern: str) -> list[str]:
518
520
  pathname = os.path.join(path, pattern)
519
521
  paths = glob.glob(pathname, recursive=True)
520
522
  # normalize paths to use OS-specific separators
521
- paths = [os.path.normpath(path_result) for path_result in paths]
523
+ paths = [
524
+ os.path.relpath(os.path.normpath(path_result), path) for path_result in paths
525
+ ]
522
526
  return paths
523
527
 
524
528
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fsutil
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: high-level file-system operations for lazy devs.
5
5
  Author-email: Fabio Caccamo <fabio.caccamo@gmail.com>
6
6
  Maintainer-email: Fabio Caccamo <fabio.caccamo@gmail.com>
@@ -69,6 +69,7 @@ Dynamic: license-file
69
69
  [![](https://img.shields.io/codacy/grade/fc40788ae7d74d1fb34a38934113c4e5?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/python-fsutil)
70
70
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
71
71
  [![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
72
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fabiocaccamo/python-fsutil/badge)](https://securityscorecards.dev/viewer/?uri=github.com/fabiocaccamo/python-fsutil)
72
73
 
73
74
  # python-fsutil
74
75
  high-level file-system operations for lazy devs.
@@ -13,6 +13,9 @@ import fsutil
13
13
  (1073741824, "1.00 GB"),
14
14
  (1879048192, "1.75 GB"),
15
15
  (1099511627776, "1.00 TB"),
16
+ (2**80, "1.00 YB"),
17
+ (2**90, "1024.00 YB"),
18
+ (2**100, "1048576.00 YB"),
16
19
  ],
17
20
  )
18
21
  def test_convert_size_bytes_to_string(size_bytes, expected_output):
@@ -51,6 +54,9 @@ def test_convert_size_bytes_to_string_and_convert_size_string_to_bytes(
51
54
  ("1.00 MB", 1048576),
52
55
  ("1.00 GB", 1073741824),
53
56
  ("1.00 TB", 1099511627776),
57
+ ("1.00 YB", 2**80),
58
+ ("1024.00 YB", 2**90),
59
+ ("1048576.00 YB", 2**100),
54
60
  ],
55
61
  )
56
62
  def test_convert_size_string_to_bytes(size_string, expected_output):
@@ -69,6 +75,9 @@ def test_convert_size_string_to_bytes(size_string, expected_output):
69
75
  (1170378588, 1170378588),
70
76
  (2136746229, 2136746229),
71
77
  (1099511627776, 1099511627776),
78
+ (2**80, 2**80),
79
+ (2**90, 2**90),
80
+ (2**100, 2**100),
72
81
  ],
73
82
  )
74
83
  def test_convert_size_string_to_bytes_and_convert_size_bytes_to_string(
@@ -1,3 +1,4 @@
1
+ import io
1
2
  import re
2
3
  import time
3
4
  from datetime import datetime, timedelta
@@ -127,6 +128,20 @@ def test_get_file_hash(temp_path):
127
128
  assert file_hash == "b10a8db164e0754105b7a99be72e3fe5"
128
129
 
129
130
 
131
+ def test_get_file_hash_with_file_like_object():
132
+ file = io.BytesIO(b"Hello World")
133
+ file_hash = fsutil.get_file_hash(file)
134
+ assert file_hash == "b10a8db164e0754105b7a99be72e3fe5"
135
+
136
+
137
+ def test_get_file_hash_with_file_like_object_restores_position():
138
+ file = io.BytesIO(b"Hello World")
139
+ file.seek(6)
140
+ file_hash = fsutil.get_file_hash(file)
141
+ assert file_hash == "b10a8db164e0754105b7a99be72e3fe5"
142
+ assert file.tell() == 6
143
+
144
+
130
145
  def test_get_file_last_modified_date(temp_path):
131
146
  path = temp_path("a/b/c.txt")
132
147
  fsutil.create_file(path, content="Hello")
@@ -232,6 +232,33 @@ def test_download_file_without_requests_installed(temp_path):
232
232
  fsutil.download_file(url, dirpath=temp_path())
233
233
 
234
234
 
235
+ def test_download_file_content_disposition_filename_is_sanitized(temp_path):
236
+ class MockResponse:
237
+ headers = {"content-disposition": 'attachment; filename="..\\..\\evil.txt"'}
238
+
239
+ def __enter__(self):
240
+ return self
241
+
242
+ def __exit__(self, exc_type, exc_val, exc_tb):
243
+ return None
244
+
245
+ def raise_for_status(self):
246
+ return None
247
+
248
+ def iter_content(self, chunk_size=1):
249
+ yield b"hello world"
250
+
251
+ class MockRequests:
252
+ def get(self, *args, **kwargs):
253
+ return MockResponse()
254
+
255
+ with patch("fsutil.operations.require_requests", return_value=MockRequests()):
256
+ path = fsutil.download_file("https://example.com/file.txt", dirpath=temp_path())
257
+ assert fsutil.exists(path)
258
+ assert fsutil.get_filename(path) == "evil.txt"
259
+ assert path == temp_path("evil.txt")
260
+
261
+
235
262
  def test_list_dirs(temp_path):
236
263
  for i in range(0, 5):
237
264
  fsutil.create_dir(temp_path(f"a/b/c/d-{i}"))
@@ -518,6 +545,24 @@ def test_search_files(temp_path):
518
545
  assert results == expected_results
519
546
 
520
547
 
548
+ def test_search_files_with_relative_path(temp_path, monkeypatch):
549
+ """
550
+ Regression test: search_files with a relative path must not return an empty
551
+ list due to the double-prefix bug in _filter_paths.
552
+ """
553
+ fsutil.create_file(temp_path("a/b/c/IMG_1002.png"))
554
+ fsutil.create_file(temp_path("a/x/c/IMG_1005.png"))
555
+ fsutil.create_file(temp_path("a/b/c/IMG_1000.jpg"))
556
+
557
+ # change cwd to the parent of "a/" so that the relative path is valid
558
+ monkeypatch.chdir(temp_path(""))
559
+
560
+ results = fsutil.search_files("./a/", "**/c/IMG_*.png")
561
+
562
+ assert len(results) == 2
563
+ assert all(fsutil.is_file(result) for result in results)
564
+
565
+
521
566
  def test_search_dirs(temp_path):
522
567
  fsutil.create_file(temp_path("a/b/c/IMG_1000.jpg"))
523
568
  fsutil.create_file(temp_path("x/y/z/c/IMG_1001.jpg"))
File without changes
File without changes