zerofilesystem 0.1.0__py3-none-any.whl
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.
- zerofilesystem/__init__.py +326 -0
- zerofilesystem/_platform.py +13 -0
- zerofilesystem/classes/__init__.py +84 -0
- zerofilesystem/classes/_internal.py +107 -0
- zerofilesystem/classes/archive_handler.py +348 -0
- zerofilesystem/classes/directory_ops.py +450 -0
- zerofilesystem/classes/exceptions.py +164 -0
- zerofilesystem/classes/file_lock.py +171 -0
- zerofilesystem/classes/file_permissions.py +320 -0
- zerofilesystem/classes/file_transaction.py +377 -0
- zerofilesystem/classes/files.py +444 -0
- zerofilesystem/classes/finder.py +603 -0
- zerofilesystem/classes/integrity_checker.py +391 -0
- zerofilesystem/classes/io.py +374 -0
- zerofilesystem/classes/path_utils.py +301 -0
- zerofilesystem/classes/secure_ops.py +255 -0
- zerofilesystem/classes/watcher.py +996 -0
- zerofilesystem/py.typed +0 -0
- zerofilesystem/zerofilesystem.py +608 -0
- zerofilesystem-0.1.0.dist-info/METADATA +383 -0
- zerofilesystem-0.1.0.dist-info/RECORD +23 -0
- zerofilesystem-0.1.0.dist-info/WHEEL +4 -0
- zerofilesystem-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""zerofilesystem - Cross-platform file system utilities.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
import zerofilesystem as zo
|
|
5
|
+
|
|
6
|
+
# Read/write files
|
|
7
|
+
zo.write_text("file.txt", "Hello World")
|
|
8
|
+
content = zo.read_text("file.txt")
|
|
9
|
+
|
|
10
|
+
# JSON operations
|
|
11
|
+
zo.write_json("data.json", {"key": "value"})
|
|
12
|
+
data = zo.read_json("data.json")
|
|
13
|
+
|
|
14
|
+
# Find files
|
|
15
|
+
py_files = zo.find_files(".", pattern="**/*.py")
|
|
16
|
+
|
|
17
|
+
# File locking
|
|
18
|
+
with zo.FileLock("/tmp/my.lock"):
|
|
19
|
+
# Critical section
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# Transactions
|
|
23
|
+
with zo.FileTransaction() as tx:
|
|
24
|
+
tx.write_text("file1.txt", "content1")
|
|
25
|
+
tx.write_text("file2.txt", "content2")
|
|
26
|
+
|
|
27
|
+
# Archives
|
|
28
|
+
zo.create_zip("./src", "backup.zip")
|
|
29
|
+
zo.extract("backup.zip", "./extracted")
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2025 Francesco Favi
|
|
32
|
+
License: MIT
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from zerofilesystem._platform import IS_LINUX, IS_MACOS, IS_UNIX, IS_WINDOWS, Pathish
|
|
36
|
+
from zerofilesystem.classes import (
|
|
37
|
+
ArchiveError,
|
|
38
|
+
ArchiveHandler,
|
|
39
|
+
DirectoryOps,
|
|
40
|
+
EventType,
|
|
41
|
+
FileCleaner,
|
|
42
|
+
FileFinder,
|
|
43
|
+
FileHasher,
|
|
44
|
+
FileIO,
|
|
45
|
+
FileLock,
|
|
46
|
+
FileLockedError,
|
|
47
|
+
FileMeta,
|
|
48
|
+
FilePermissions,
|
|
49
|
+
FileSync,
|
|
50
|
+
FileTransaction,
|
|
51
|
+
FileUtils,
|
|
52
|
+
FileWatcher,
|
|
53
|
+
Finder,
|
|
54
|
+
GzipHandler,
|
|
55
|
+
HashMismatchError,
|
|
56
|
+
IntegrityChecker,
|
|
57
|
+
IntegrityError,
|
|
58
|
+
InvalidPathError,
|
|
59
|
+
JsonHandler,
|
|
60
|
+
PathUtils,
|
|
61
|
+
PermissionDeniedError,
|
|
62
|
+
SecureDeleteError,
|
|
63
|
+
SecureOps,
|
|
64
|
+
SyncError,
|
|
65
|
+
TransactionError,
|
|
66
|
+
Watcher,
|
|
67
|
+
WatchEvent,
|
|
68
|
+
ZeroOSError,
|
|
69
|
+
)
|
|
70
|
+
from zerofilesystem.zerofilesystem import ZeroOS
|
|
71
|
+
|
|
72
|
+
__version__ = "0.1.0"
|
|
73
|
+
__author__ = "Francesco Favi"
|
|
74
|
+
__email__ = "14098835+francescofavi@users.noreply.github.com"
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# BASIC I/O
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
read_text = FileIO.read_text
|
|
81
|
+
write_text = FileIO.write_text
|
|
82
|
+
read_bytes = FileIO.read_bytes
|
|
83
|
+
write_bytes = FileIO.write_bytes
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# JSON
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
read_json = JsonHandler.read_json
|
|
90
|
+
write_json = JsonHandler.write_json
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# GZIP
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
gzip_compress = GzipHandler.compress
|
|
97
|
+
gzip_decompress = GzipHandler.decompress
|
|
98
|
+
|
|
99
|
+
# =============================================================================
|
|
100
|
+
# DISCOVERY
|
|
101
|
+
# =============================================================================
|
|
102
|
+
|
|
103
|
+
find_files = FileFinder.find_files
|
|
104
|
+
walk_files = FileFinder.walk_files
|
|
105
|
+
is_hidden = FileFinder.is_hidden
|
|
106
|
+
|
|
107
|
+
# =============================================================================
|
|
108
|
+
# CLEANUP
|
|
109
|
+
# =============================================================================
|
|
110
|
+
|
|
111
|
+
delete_files = FileCleaner.delete_files
|
|
112
|
+
delete_empty_dirs = FileCleaner.delete_empty_dirs
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# SYNC
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
move_if_absent = FileSync.move_if_absent
|
|
119
|
+
copy_if_newer = FileSync.copy_if_newer
|
|
120
|
+
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# HASH
|
|
123
|
+
# =============================================================================
|
|
124
|
+
|
|
125
|
+
file_hash = FileHasher.file_hash
|
|
126
|
+
|
|
127
|
+
# =============================================================================
|
|
128
|
+
# META
|
|
129
|
+
# =============================================================================
|
|
130
|
+
|
|
131
|
+
ensure_dir = FileMeta.ensure_dir
|
|
132
|
+
touch = FileMeta.touch
|
|
133
|
+
file_size = FileMeta.file_size
|
|
134
|
+
disk_usage = FileMeta.disk_usage
|
|
135
|
+
|
|
136
|
+
# =============================================================================
|
|
137
|
+
# UTILS
|
|
138
|
+
# =============================================================================
|
|
139
|
+
|
|
140
|
+
safe_filename = FileUtils.safe_filename
|
|
141
|
+
atomic_write = FileUtils.atomic_write
|
|
142
|
+
|
|
143
|
+
# =============================================================================
|
|
144
|
+
# PATH UTILS
|
|
145
|
+
# =============================================================================
|
|
146
|
+
|
|
147
|
+
normalize_path = PathUtils.normalize
|
|
148
|
+
to_absolute = PathUtils.to_absolute
|
|
149
|
+
to_relative = PathUtils.to_relative
|
|
150
|
+
to_posix = PathUtils.to_posix
|
|
151
|
+
expand_path = PathUtils.expand
|
|
152
|
+
is_subpath = PathUtils.is_subpath
|
|
153
|
+
common_path = PathUtils.common_path
|
|
154
|
+
validate_path = PathUtils.validate_path
|
|
155
|
+
|
|
156
|
+
# =============================================================================
|
|
157
|
+
# PERMISSIONS
|
|
158
|
+
# =============================================================================
|
|
159
|
+
|
|
160
|
+
get_metadata = FilePermissions.get_metadata
|
|
161
|
+
set_readonly = FilePermissions.set_readonly
|
|
162
|
+
set_hidden = FilePermissions.set_hidden
|
|
163
|
+
set_executable = FilePermissions.set_executable
|
|
164
|
+
set_permissions = FilePermissions.set_permissions
|
|
165
|
+
copy_permissions = FilePermissions.copy_permissions
|
|
166
|
+
set_timestamps = FilePermissions.set_timestamps
|
|
167
|
+
mode_to_string = FilePermissions.mode_to_string
|
|
168
|
+
string_to_mode = FilePermissions.string_to_mode
|
|
169
|
+
|
|
170
|
+
# =============================================================================
|
|
171
|
+
# DIRECTORY OPS
|
|
172
|
+
# =============================================================================
|
|
173
|
+
|
|
174
|
+
copy_tree = DirectoryOps.copy_tree
|
|
175
|
+
move_tree = DirectoryOps.move_tree
|
|
176
|
+
sync_dirs = DirectoryOps.sync
|
|
177
|
+
temp_directory = DirectoryOps.temp_directory
|
|
178
|
+
tree_size = DirectoryOps.tree_size
|
|
179
|
+
tree_file_count = DirectoryOps.tree_file_count
|
|
180
|
+
flatten_tree = DirectoryOps.flatten
|
|
181
|
+
|
|
182
|
+
# =============================================================================
|
|
183
|
+
# INTEGRITY
|
|
184
|
+
# =============================================================================
|
|
185
|
+
|
|
186
|
+
directory_hash = IntegrityChecker.directory_hash
|
|
187
|
+
create_manifest = IntegrityChecker.create_manifest
|
|
188
|
+
save_manifest = IntegrityChecker.save_manifest
|
|
189
|
+
load_manifest = IntegrityChecker.load_manifest
|
|
190
|
+
verify_manifest = IntegrityChecker.verify_manifest
|
|
191
|
+
verify_file = IntegrityChecker.verify_file
|
|
192
|
+
compare_directories = IntegrityChecker.compare_directories
|
|
193
|
+
snapshot_hash = IntegrityChecker.snapshot_hash
|
|
194
|
+
|
|
195
|
+
# =============================================================================
|
|
196
|
+
# SECURE
|
|
197
|
+
# =============================================================================
|
|
198
|
+
|
|
199
|
+
secure_delete = SecureOps.secure_delete
|
|
200
|
+
secure_delete_directory = SecureOps.secure_delete_directory
|
|
201
|
+
private_directory = SecureOps.private_directory
|
|
202
|
+
create_private_file = SecureOps.create_private_file
|
|
203
|
+
|
|
204
|
+
# =============================================================================
|
|
205
|
+
# ARCHIVE
|
|
206
|
+
# =============================================================================
|
|
207
|
+
|
|
208
|
+
create_tar = ArchiveHandler.create_tar
|
|
209
|
+
create_zip = ArchiveHandler.create_zip
|
|
210
|
+
extract_tar = ArchiveHandler.extract_tar
|
|
211
|
+
extract_zip = ArchiveHandler.extract_zip
|
|
212
|
+
extract = ArchiveHandler.extract
|
|
213
|
+
list_archive = ArchiveHandler.list_archive
|
|
214
|
+
|
|
215
|
+
# =============================================================================
|
|
216
|
+
# __all__
|
|
217
|
+
# =============================================================================
|
|
218
|
+
|
|
219
|
+
__all__ = [
|
|
220
|
+
# Facade class
|
|
221
|
+
"ZeroOS",
|
|
222
|
+
# Platform constants
|
|
223
|
+
"IS_WINDOWS",
|
|
224
|
+
"IS_MACOS",
|
|
225
|
+
"IS_LINUX",
|
|
226
|
+
"IS_UNIX",
|
|
227
|
+
"Pathish",
|
|
228
|
+
# Classes (for advanced usage)
|
|
229
|
+
"Finder",
|
|
230
|
+
"Watcher",
|
|
231
|
+
"WatchEvent",
|
|
232
|
+
"EventType",
|
|
233
|
+
"FileLock",
|
|
234
|
+
"FileTransaction",
|
|
235
|
+
"FileWatcher", # Legacy
|
|
236
|
+
# Exceptions
|
|
237
|
+
"ZeroOSError",
|
|
238
|
+
"FileLockedError",
|
|
239
|
+
"InvalidPathError",
|
|
240
|
+
"HashMismatchError",
|
|
241
|
+
"IntegrityError",
|
|
242
|
+
"TransactionError",
|
|
243
|
+
"ArchiveError",
|
|
244
|
+
"PermissionDeniedError",
|
|
245
|
+
"SecureDeleteError",
|
|
246
|
+
"SyncError",
|
|
247
|
+
# Basic I/O
|
|
248
|
+
"read_text",
|
|
249
|
+
"write_text",
|
|
250
|
+
"read_bytes",
|
|
251
|
+
"write_bytes",
|
|
252
|
+
# JSON
|
|
253
|
+
"read_json",
|
|
254
|
+
"write_json",
|
|
255
|
+
# Gzip
|
|
256
|
+
"gzip_compress",
|
|
257
|
+
"gzip_decompress",
|
|
258
|
+
# Discovery
|
|
259
|
+
"find_files",
|
|
260
|
+
"walk_files",
|
|
261
|
+
"is_hidden",
|
|
262
|
+
# Cleanup
|
|
263
|
+
"delete_files",
|
|
264
|
+
"delete_empty_dirs",
|
|
265
|
+
# Sync
|
|
266
|
+
"move_if_absent",
|
|
267
|
+
"copy_if_newer",
|
|
268
|
+
# Hash
|
|
269
|
+
"file_hash",
|
|
270
|
+
# Meta
|
|
271
|
+
"ensure_dir",
|
|
272
|
+
"touch",
|
|
273
|
+
"file_size",
|
|
274
|
+
"disk_usage",
|
|
275
|
+
# Utils
|
|
276
|
+
"safe_filename",
|
|
277
|
+
"atomic_write",
|
|
278
|
+
# Path utils
|
|
279
|
+
"normalize_path",
|
|
280
|
+
"to_absolute",
|
|
281
|
+
"to_relative",
|
|
282
|
+
"to_posix",
|
|
283
|
+
"expand_path",
|
|
284
|
+
"is_subpath",
|
|
285
|
+
"common_path",
|
|
286
|
+
"validate_path",
|
|
287
|
+
# Permissions
|
|
288
|
+
"get_metadata",
|
|
289
|
+
"set_readonly",
|
|
290
|
+
"set_hidden",
|
|
291
|
+
"set_executable",
|
|
292
|
+
"set_permissions",
|
|
293
|
+
"copy_permissions",
|
|
294
|
+
"set_timestamps",
|
|
295
|
+
"mode_to_string",
|
|
296
|
+
"string_to_mode",
|
|
297
|
+
# Directory ops
|
|
298
|
+
"copy_tree",
|
|
299
|
+
"move_tree",
|
|
300
|
+
"sync_dirs",
|
|
301
|
+
"temp_directory",
|
|
302
|
+
"tree_size",
|
|
303
|
+
"tree_file_count",
|
|
304
|
+
"flatten_tree",
|
|
305
|
+
# Integrity
|
|
306
|
+
"directory_hash",
|
|
307
|
+
"create_manifest",
|
|
308
|
+
"save_manifest",
|
|
309
|
+
"load_manifest",
|
|
310
|
+
"verify_manifest",
|
|
311
|
+
"verify_file",
|
|
312
|
+
"compare_directories",
|
|
313
|
+
"snapshot_hash",
|
|
314
|
+
# Secure
|
|
315
|
+
"secure_delete",
|
|
316
|
+
"secure_delete_directory",
|
|
317
|
+
"private_directory",
|
|
318
|
+
"create_private_file",
|
|
319
|
+
# Archive
|
|
320
|
+
"create_tar",
|
|
321
|
+
"create_zip",
|
|
322
|
+
"extract_tar",
|
|
323
|
+
"extract_zip",
|
|
324
|
+
"extract",
|
|
325
|
+
"list_archive",
|
|
326
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Platform detection constants and shared types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
IS_WINDOWS: bool = sys.platform == "win32"
|
|
9
|
+
IS_MACOS: bool = sys.platform == "darwin"
|
|
10
|
+
IS_LINUX: bool = sys.platform.startswith("linux")
|
|
11
|
+
IS_UNIX: bool = IS_MACOS or IS_LINUX
|
|
12
|
+
|
|
13
|
+
Pathish = str | Path
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Helper classes for file operations."""
|
|
2
|
+
|
|
3
|
+
from zerofilesystem.classes.archive_handler import ArchiveHandler
|
|
4
|
+
from zerofilesystem.classes.directory_ops import DirectoryOps
|
|
5
|
+
from zerofilesystem.classes.exceptions import (
|
|
6
|
+
ArchiveError,
|
|
7
|
+
FileLockedError,
|
|
8
|
+
HashMismatchError,
|
|
9
|
+
IntegrityError,
|
|
10
|
+
InvalidPathError,
|
|
11
|
+
PermissionDeniedError,
|
|
12
|
+
SecureDeleteError,
|
|
13
|
+
SyncError,
|
|
14
|
+
TransactionError,
|
|
15
|
+
ZeroOSError,
|
|
16
|
+
)
|
|
17
|
+
from zerofilesystem.classes.file_lock import FileLock
|
|
18
|
+
from zerofilesystem.classes.file_permissions import FilePermissions
|
|
19
|
+
from zerofilesystem.classes.file_transaction import FileTransaction
|
|
20
|
+
from zerofilesystem.classes.files import FileCleaner, FileFinder, FileHasher, FileMeta, FileSync
|
|
21
|
+
from zerofilesystem.classes.finder import Finder
|
|
22
|
+
from zerofilesystem.classes.integrity_checker import IntegrityChecker
|
|
23
|
+
from zerofilesystem.classes.io import FileIO, FileUtils, GzipHandler, JsonHandler
|
|
24
|
+
from zerofilesystem.classes.path_utils import PathUtils
|
|
25
|
+
from zerofilesystem.classes.secure_ops import SecureOps
|
|
26
|
+
from zerofilesystem.classes.watcher import (
|
|
27
|
+
EventType,
|
|
28
|
+
FileWatcher,
|
|
29
|
+
Watcher,
|
|
30
|
+
WatchEvent,
|
|
31
|
+
WatchEventType,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Legacy alias
|
|
35
|
+
WatchEventOld = WatchEvent
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Core I/O
|
|
39
|
+
"FileIO",
|
|
40
|
+
"JsonHandler",
|
|
41
|
+
"GzipHandler",
|
|
42
|
+
"FileUtils",
|
|
43
|
+
# Discovery and cleanup
|
|
44
|
+
"Finder",
|
|
45
|
+
"FileFinder",
|
|
46
|
+
"FileCleaner",
|
|
47
|
+
# Sync and file operations
|
|
48
|
+
"FileSync",
|
|
49
|
+
"FileHasher",
|
|
50
|
+
"FileMeta",
|
|
51
|
+
"FileLock",
|
|
52
|
+
# Path utilities
|
|
53
|
+
"PathUtils",
|
|
54
|
+
# Permissions and metadata
|
|
55
|
+
"FilePermissions",
|
|
56
|
+
# Directory operations
|
|
57
|
+
"DirectoryOps",
|
|
58
|
+
# Integrity verification
|
|
59
|
+
"IntegrityChecker",
|
|
60
|
+
# Transactions
|
|
61
|
+
"FileTransaction",
|
|
62
|
+
# Security
|
|
63
|
+
"SecureOps",
|
|
64
|
+
# Archives
|
|
65
|
+
"ArchiveHandler",
|
|
66
|
+
# File watching
|
|
67
|
+
"Watcher",
|
|
68
|
+
"WatchEvent",
|
|
69
|
+
"EventType",
|
|
70
|
+
"FileWatcher", # Legacy
|
|
71
|
+
"WatchEventType", # Legacy alias
|
|
72
|
+
"WatchEventOld", # Legacy alias
|
|
73
|
+
# Exceptions
|
|
74
|
+
"ZeroOSError",
|
|
75
|
+
"FileLockedError",
|
|
76
|
+
"InvalidPathError",
|
|
77
|
+
"HashMismatchError",
|
|
78
|
+
"IntegrityError",
|
|
79
|
+
"TransactionError",
|
|
80
|
+
"ArchiveError",
|
|
81
|
+
"PermissionDeniedError",
|
|
82
|
+
"SecureDeleteError",
|
|
83
|
+
"SyncError",
|
|
84
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Shared internal utilities for zerofilesystem classes.
|
|
2
|
+
|
|
3
|
+
Not part of the public API. Used by finder.py, watcher.py, and other modules
|
|
4
|
+
to avoid code duplication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from zerofilesystem._platform import IS_WINDOWS
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# SHARED CONSTANTS
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
FILE_ATTRIBUTE_HIDDEN: int = 0x2
|
|
21
|
+
FILE_ATTRIBUTE_READONLY: int = 0x1
|
|
22
|
+
|
|
23
|
+
MAX_RENAME_CONFLICTS: int = 10000
|
|
24
|
+
|
|
25
|
+
HASH_CHUNK_SIZE: int = 1024 * 1024 # 1 MB
|
|
26
|
+
|
|
27
|
+
# Size unit multipliers (bytes)
|
|
28
|
+
SIZE_UNITS: dict[str, int] = {
|
|
29
|
+
"B": 1,
|
|
30
|
+
"KB": 1024,
|
|
31
|
+
"MB": 1024 * 1024,
|
|
32
|
+
"GB": 1024 * 1024 * 1024,
|
|
33
|
+
"TB": 1024 * 1024 * 1024 * 1024,
|
|
34
|
+
"K": 1024,
|
|
35
|
+
"M": 1024 * 1024,
|
|
36
|
+
"G": 1024 * 1024 * 1024,
|
|
37
|
+
"T": 1024 * 1024 * 1024 * 1024,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# SHARED FUNCTIONS
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_size(size: int | str) -> int:
|
|
47
|
+
"""Parse size string like '1KB', '5MB', '1.5GB' to bytes."""
|
|
48
|
+
if isinstance(size, int):
|
|
49
|
+
return size
|
|
50
|
+
|
|
51
|
+
size = size.strip().upper()
|
|
52
|
+
|
|
53
|
+
match = re.match(r"^([\d.]+)\s*([A-Z]*B?)$", size)
|
|
54
|
+
if not match:
|
|
55
|
+
raise ValueError(f"Invalid size format: {size}")
|
|
56
|
+
|
|
57
|
+
value = float(match.group(1))
|
|
58
|
+
unit = match.group(2) or "B"
|
|
59
|
+
|
|
60
|
+
if unit not in SIZE_UNITS:
|
|
61
|
+
raise ValueError(f"Unknown size unit: {unit}")
|
|
62
|
+
|
|
63
|
+
return int(value * SIZE_UNITS[unit])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_datetime(dt: datetime | str | timedelta) -> datetime:
|
|
67
|
+
"""Parse datetime from various formats."""
|
|
68
|
+
if isinstance(dt, datetime):
|
|
69
|
+
return dt
|
|
70
|
+
|
|
71
|
+
if isinstance(dt, timedelta):
|
|
72
|
+
return datetime.now() - dt
|
|
73
|
+
|
|
74
|
+
dt_str = dt.strip()
|
|
75
|
+
|
|
76
|
+
formats = [
|
|
77
|
+
"%Y-%m-%d %H:%M:%S",
|
|
78
|
+
"%Y-%m-%d %H:%M",
|
|
79
|
+
"%Y-%m-%d",
|
|
80
|
+
"%Y/%m/%d %H:%M:%S",
|
|
81
|
+
"%Y/%m/%d",
|
|
82
|
+
"%d-%m-%Y",
|
|
83
|
+
"%d/%m/%Y",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
for fmt in formats:
|
|
87
|
+
try:
|
|
88
|
+
return datetime.strptime(dt_str, fmt)
|
|
89
|
+
except ValueError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
raise ValueError(f"Cannot parse datetime: {dt}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_hidden(path: Path) -> bool:
|
|
96
|
+
"""Check if file is hidden (cross-platform)."""
|
|
97
|
+
if path.name.startswith("."):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
if IS_WINDOWS:
|
|
101
|
+
try:
|
|
102
|
+
attrs = os.stat(path).st_file_attributes # type: ignore[attr-defined]
|
|
103
|
+
return bool(attrs & FILE_ATTRIBUTE_HIDDEN)
|
|
104
|
+
except (AttributeError, OSError):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
return False
|