pseek 2.5.1__tar.gz → 2.5.2__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.
- {pseek-2.5.1 → pseek-2.5.2}/PKG-INFO +3 -2
- {pseek-2.5.1 → pseek-2.5.2}/README.md +2 -1
- {pseek-2.5.1 → pseek-2.5.2}/pseek/cli.py +5 -2
- {pseek-2.5.1 → pseek-2.5.2}/pseek/searcher.py +185 -132
- {pseek-2.5.1 → pseek-2.5.2}/pseek/utils.py +4 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek.egg-info/PKG-INFO +3 -2
- {pseek-2.5.1 → pseek-2.5.2}/pyproject.toml +1 -1
- {pseek-2.5.1 → pseek-2.5.2}/LICENSE +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek/__init__.py +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek/parser.py +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek.egg-info/SOURCES.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek.egg-info/dependency_links.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek.egg-info/entry_points.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek.egg-info/requires.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/pseek.egg-info/top_level.txt +0 -0
- {pseek-2.5.1 → pseek-2.5.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.2
|
|
4
4
|
Summary: Pseek is a Python library to search files, folders, and text
|
|
5
5
|
Author-email: Arian <ariannasiri86@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -102,6 +102,7 @@ pseek "error\d+" --regex
|
|
|
102
102
|
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
103
103
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
104
104
|
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
105
|
+
| `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
|
|
105
106
|
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
106
107
|
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
107
108
|
| `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
|
|
@@ -112,4 +113,4 @@ pseek "error\d+" --regex
|
|
|
112
113
|
## Requirements
|
|
113
114
|
|
|
114
115
|
* Python 3.6 or higher
|
|
115
|
-
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
116
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
|
|
@@ -85,6 +85,7 @@ pseek "error\d+" --regex
|
|
|
85
85
|
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
86
86
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
87
87
|
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
88
|
+
| `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
|
|
88
89
|
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
89
90
|
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
90
91
|
| `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
|
|
@@ -95,4 +96,4 @@ pseek "error\d+" --regex
|
|
|
95
96
|
## Requirements
|
|
96
97
|
|
|
97
98
|
* Python 3.6 or higher
|
|
98
|
-
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
99
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
|
|
@@ -77,6 +77,8 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
77
77
|
# Archive options
|
|
78
78
|
@click.option('--archive', is_flag=True,
|
|
79
79
|
help='Enable search within archive files (e.g. zip, rar, 7z, gz, bz2, xz, tar, tar.gz, tar.bz2, tar.xz)')
|
|
80
|
+
@click.option('--depth', type=click.IntRange(min=0), show_default=True,
|
|
81
|
+
help='Maximum archive depth to recurse into (e.g. 2 means only 2 levels).')
|
|
80
82
|
@click.option('--arc-ext', multiple=True, type=click.STRING,
|
|
81
83
|
help='Include files with these extensions inside archive files. Example: --arc-ext py --arc-ext js')
|
|
82
84
|
@click.option('--arc-ee', multiple=True, type=click.STRING,
|
|
@@ -94,8 +96,8 @@ def run_search_process(file, directory, content, search_instance):
|
|
|
94
96
|
@click.option('--full-path', is_flag=True, help='Display full paths for results.')
|
|
95
97
|
@click.option('--no-content', is_flag=True, help='Only display files path for content search.')
|
|
96
98
|
def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex, include, exclude,
|
|
97
|
-
re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive,
|
|
98
|
-
arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
|
|
99
|
+
re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive, depth,
|
|
100
|
+
arc_ext, arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
|
|
99
101
|
"""Search for files, directories, and file content based on the query."""
|
|
100
102
|
|
|
101
103
|
check_rar_backend(archive, rarfb, query)
|
|
@@ -140,6 +142,7 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
|
|
|
140
142
|
max_size=max_size,
|
|
141
143
|
min_size=min_size,
|
|
142
144
|
archive=archive,
|
|
145
|
+
depth=depth,
|
|
143
146
|
arc_ext=arc_ext,
|
|
144
147
|
arc_ee=arc_ee,
|
|
145
148
|
arc_inc=arc_inc,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import mmap
|
|
2
2
|
import click
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from .utils import compile_regex, get_archive_path_size, try_decode
|
|
4
|
+
from .utils import compile_regex, get_archive_path_size, try_decode, get_path_suffix
|
|
5
5
|
from .parser import parse_query_expression, TermNode, highlight_text
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
7
|
import zipfile
|
|
@@ -11,9 +11,10 @@ import gzip
|
|
|
11
11
|
import bz2
|
|
12
12
|
import lzma
|
|
13
13
|
import rarfile
|
|
14
|
+
import io
|
|
14
15
|
|
|
15
16
|
# Archive extensions that are allowed
|
|
16
|
-
ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', '
|
|
17
|
+
ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz', 'gz', 'bz2', 'xz')
|
|
17
18
|
|
|
18
19
|
# Extensions that are not suitable for content search (binary, media, etc.)
|
|
19
20
|
EXCLUDED_EXTENSIONS = (
|
|
@@ -30,7 +31,7 @@ EXCLUDED_EXTENSIONS = (
|
|
|
30
31
|
|
|
31
32
|
class Search:
|
|
32
33
|
def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, re_include,
|
|
33
|
-
re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, arc_ext, arc_ee,
|
|
34
|
+
re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, depth, arc_ext, arc_ee,
|
|
34
35
|
arc_inc, arc_exc, arc_max, arc_min, full_path, no_content):
|
|
35
36
|
"""Initialize search parameters"""
|
|
36
37
|
self.base_path = Path(base_path)
|
|
@@ -50,6 +51,7 @@ class Search:
|
|
|
50
51
|
self.max_size = max_size
|
|
51
52
|
self.min_size = min_size
|
|
52
53
|
self.archive = archive
|
|
54
|
+
self.depth = depth
|
|
53
55
|
self.arc_ext = set(arc_ext)
|
|
54
56
|
self.arc_ee = set(arc_ee) | {''} if arc_ee else set()
|
|
55
57
|
self.arc_inc = {Path(p) for p in arc_inc}
|
|
@@ -71,10 +73,10 @@ class Search:
|
|
|
71
73
|
# If path is inaccessible, skip it.
|
|
72
74
|
return True
|
|
73
75
|
|
|
74
|
-
file_ext =
|
|
76
|
+
file_ext = get_path_suffix(p_resolved)
|
|
75
77
|
|
|
76
78
|
# Ignore some filters for archive files when archive is enabled
|
|
77
|
-
if (not self.archive or not file_ext in
|
|
79
|
+
if (not self.archive or not file_ext in ARCHIVE_EXTS[:-3]) and \
|
|
78
80
|
((search_type in ('file', 'content') and not p_resolved.is_file())
|
|
79
81
|
or (search_type == 'directory' and not p_resolved.is_dir())):
|
|
80
82
|
return True
|
|
@@ -96,11 +98,10 @@ class Search:
|
|
|
96
98
|
|
|
97
99
|
return False
|
|
98
100
|
|
|
99
|
-
|
|
100
101
|
def archive_should_skip(self, path_info: Path, search_type: str, is_file: bool, is_dir: bool, p_size: float):
|
|
101
102
|
"""Check whether the file/directory inside archive files should be skipped based on various filters"""
|
|
102
103
|
|
|
103
|
-
file_ext =
|
|
104
|
+
file_ext = get_path_suffix(path_info)
|
|
104
105
|
|
|
105
106
|
if (search_type in ('file', 'content') and not is_file) \
|
|
106
107
|
or (search_type == 'directory' and not is_dir):
|
|
@@ -122,38 +123,57 @@ class Search:
|
|
|
122
123
|
|
|
123
124
|
return False
|
|
124
125
|
|
|
125
|
-
def extract_names_from_archive(self, file_path: Path, search_type: str
|
|
126
|
-
|
|
126
|
+
def extract_names_from_archive(self, file_path: Path, search_type: str,
|
|
127
|
+
file_bytes: bytes = None, parent_label: str = ''):
|
|
128
|
+
"""
|
|
129
|
+
Recursively extract files and directories name from archive files.
|
|
130
|
+
Supports nested archives like a.zip::b.7z::c.txt.
|
|
131
|
+
|
|
132
|
+
Parameters:
|
|
133
|
+
file_path (Path): the archive file path
|
|
134
|
+
search_type (str): search type ( file / directory )
|
|
135
|
+
file_bytes (bytes | None): optional byte data if already read (for recursion)
|
|
136
|
+
parent_label (str): string for nested archive tracking like a.zip::b.7z::file.txt
|
|
137
|
+
|
|
138
|
+
Yields:
|
|
139
|
+
(str, Path): tuple of parent label and file or directory name
|
|
140
|
+
"""
|
|
127
141
|
|
|
128
|
-
file_ext =
|
|
142
|
+
file_ext = get_path_suffix(file_path)
|
|
143
|
+
label_prefix = (parent_label + str(file_path) + '::') if parent_label else '::'
|
|
129
144
|
|
|
130
145
|
try:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
# Decide the stream source: from disk or memory
|
|
147
|
+
file_stream = io.BytesIO(file_bytes) if file_bytes is not None else open(file_path, 'rb')
|
|
148
|
+
|
|
149
|
+
# Handle ZIP and RAR archives
|
|
150
|
+
if file_ext in ('zip', 'rar'):
|
|
151
|
+
opener = {'zip': zipfile.ZipFile, 'rar': rarfile.RarFile}[file_ext]
|
|
152
|
+
with opener(file_stream) as f:
|
|
153
|
+
for info in f.infolist():
|
|
134
154
|
name = Path(info.filename)
|
|
135
155
|
if not self.archive_should_skip(
|
|
136
156
|
name,
|
|
137
157
|
search_type,
|
|
138
158
|
not info.is_dir(),
|
|
139
159
|
info.is_dir(),
|
|
140
|
-
get_archive_path_size(info,
|
|
160
|
+
get_archive_path_size(info, file_ext)
|
|
141
161
|
):
|
|
142
|
-
yield name
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if
|
|
162
|
+
yield label_prefix, name
|
|
163
|
+
|
|
164
|
+
# At each recursion, subtract 1 from depth if it's set
|
|
165
|
+
new_depth = None if self.depth is None else self.depth - 1
|
|
166
|
+
# Check if this is a nested archive
|
|
167
|
+
if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
|
|
168
|
+
yield from self.extract_names_from_archive(
|
|
148
169
|
name,
|
|
149
170
|
search_type,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
yield name
|
|
171
|
+
f.read(info),
|
|
172
|
+
label_prefix
|
|
173
|
+
)
|
|
174
|
+
# Handle 7Z archives
|
|
155
175
|
elif file_ext == '7z':
|
|
156
|
-
with py7zr.SevenZipFile(
|
|
176
|
+
with py7zr.SevenZipFile(file_stream, mode='r') as z:
|
|
157
177
|
for info in z.list():
|
|
158
178
|
name = Path(info.filename)
|
|
159
179
|
if not self.archive_should_skip(
|
|
@@ -163,18 +183,31 @@ class Search:
|
|
|
163
183
|
info.is_directory,
|
|
164
184
|
get_archive_path_size(info, '7z')
|
|
165
185
|
):
|
|
166
|
-
yield name
|
|
186
|
+
yield label_prefix, name
|
|
187
|
+
|
|
188
|
+
new_depth = None if self.depth is None else self.depth - 1
|
|
189
|
+
if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
|
|
190
|
+
file_data = z.read([info.filename]).get(info.filename)
|
|
191
|
+
if file_data is None:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
yield from self.extract_names_from_archive(
|
|
195
|
+
name,
|
|
196
|
+
search_type,
|
|
197
|
+
file_data.read(),
|
|
198
|
+
label_prefix
|
|
199
|
+
)
|
|
200
|
+
# Handle TAR and compressed TAR formats
|
|
167
201
|
elif file_ext in ('tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
|
|
168
202
|
# Specify the mode based on the file ext to open it
|
|
169
|
-
mode =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
with tarfile.open(file_path, mode) as tf:
|
|
203
|
+
mode = {
|
|
204
|
+
'tar': 'r',
|
|
205
|
+
'tar.gz': 'r:gz',
|
|
206
|
+
'tar.bz2': 'r:bz2',
|
|
207
|
+
'tar.xz': 'r:xz'
|
|
208
|
+
}[file_ext]
|
|
209
|
+
|
|
210
|
+
with tarfile.open(fileobj=file_stream, mode=mode) as tf:
|
|
178
211
|
for member in tf.getmembers():
|
|
179
212
|
name = Path(member.name)
|
|
180
213
|
if not self.archive_should_skip(
|
|
@@ -184,120 +217,139 @@ class Search:
|
|
|
184
217
|
member.isdir(),
|
|
185
218
|
get_archive_path_size(member, file_ext)
|
|
186
219
|
):
|
|
187
|
-
yield name
|
|
220
|
+
yield label_prefix, name
|
|
221
|
+
|
|
222
|
+
new_depth = None if self.depth is None else self.depth - 1
|
|
223
|
+
if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
|
|
224
|
+
f = tf.extractfile(member)
|
|
225
|
+
if f is None:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
yield from self.extract_names_from_archive(
|
|
229
|
+
name,
|
|
230
|
+
search_type,
|
|
231
|
+
f.read(),
|
|
232
|
+
label_prefix
|
|
233
|
+
)
|
|
188
234
|
except Exception:
|
|
189
235
|
return # silently skip invalid archives
|
|
190
236
|
|
|
191
|
-
def extract_text_from_archive(self, file_path: Path):
|
|
237
|
+
def extract_text_from_archive(self, file_path: Path, file_bytes: bytes = None, parent_label: str = ''):
|
|
192
238
|
"""
|
|
193
|
-
|
|
194
|
-
Supports
|
|
239
|
+
Recursively extract (path_label, text_content) from any archive file.
|
|
240
|
+
Supports nested archives like a.zip::b.7z::c.txt.
|
|
241
|
+
|
|
242
|
+
Parameters:
|
|
243
|
+
file_path (Path): the archive file path
|
|
244
|
+
file_bytes (bytes | None): optional byte data if already read (for recursion)
|
|
245
|
+
parent_label (str): string for nested archive tracking like a.zip::b.7z::file.txt
|
|
246
|
+
|
|
247
|
+
Yields:
|
|
248
|
+
(str, str): tuple of full virtual path and decoded content text
|
|
195
249
|
"""
|
|
196
250
|
|
|
197
|
-
file_ext =
|
|
251
|
+
file_ext = get_path_suffix(file_path)
|
|
252
|
+
label_prefix = (parent_label + str(file_path) + '::') if parent_label else '::'
|
|
198
253
|
|
|
199
254
|
try:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
info.is_dir(),
|
|
209
|
-
get_archive_path_size(info, 'zip')
|
|
210
|
-
):
|
|
211
|
-
continue
|
|
212
|
-
|
|
213
|
-
with zf.open(info) as f:
|
|
214
|
-
data = f.read()
|
|
215
|
-
text = try_decode(data)
|
|
216
|
-
if text is not None:
|
|
217
|
-
yield info.filename, text
|
|
218
|
-
elif file_ext == 'rar':
|
|
219
|
-
with rarfile.RarFile(file_path) as rf:
|
|
220
|
-
for info in rf.infolist():
|
|
255
|
+
# Decide the stream source: from disk or memory
|
|
256
|
+
file_stream = io.BytesIO(file_bytes) if file_bytes is not None else open(file_path, 'rb')
|
|
257
|
+
|
|
258
|
+
# Handle ZIP and RAR archives
|
|
259
|
+
if file_ext in ('zip', 'rar'):
|
|
260
|
+
opener = {'zip': zipfile.ZipFile, 'rar': rarfile.RarFile}[file_ext]
|
|
261
|
+
with opener(file_stream) as f:
|
|
262
|
+
for info in f.infolist():
|
|
221
263
|
file_name = Path(info.filename)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
264
|
+
data = f.read(info)
|
|
265
|
+
# At each recursion, subtract 1 from depth if it's set
|
|
266
|
+
new_depth = None if self.depth is None else self.depth - 1
|
|
267
|
+
|
|
268
|
+
# Check if this is a nested archive
|
|
269
|
+
if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
|
|
270
|
+
yield from self.extract_text_from_archive(file_name, data, label_prefix)
|
|
271
|
+
else:
|
|
272
|
+
if self.archive_should_skip(
|
|
273
|
+
file_name,
|
|
274
|
+
'content',
|
|
275
|
+
not info.is_dir(),
|
|
276
|
+
info.is_dir(),
|
|
277
|
+
get_archive_path_size(info, file_ext)
|
|
278
|
+
):
|
|
279
|
+
continue
|
|
230
280
|
|
|
231
|
-
with rf.open(info) as f:
|
|
232
|
-
data = f.read()
|
|
233
281
|
text = try_decode(data)
|
|
234
282
|
if text is not None:
|
|
235
|
-
yield
|
|
283
|
+
yield label_prefix + str(file_name), text
|
|
284
|
+
# Handle 7Z archives
|
|
236
285
|
elif file_ext == '7z':
|
|
237
|
-
with py7zr.SevenZipFile(
|
|
286
|
+
with py7zr.SevenZipFile(file_stream, mode='r') as archive:
|
|
238
287
|
for info in archive.list():
|
|
239
|
-
|
|
240
|
-
if
|
|
241
|
-
file_name,
|
|
242
|
-
'content',
|
|
243
|
-
not info.is_directory,
|
|
244
|
-
info.is_directory,
|
|
245
|
-
get_archive_path_size(info, '7z')
|
|
246
|
-
):
|
|
288
|
+
file_data = archive.read([info.filename]).get(info.filename)
|
|
289
|
+
if file_data is None:
|
|
247
290
|
continue
|
|
248
291
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
292
|
+
file_name = Path(info.filename)
|
|
293
|
+
data = file_data.read()
|
|
294
|
+
new_depth = None if self.depth is None else self.depth - 1
|
|
295
|
+
|
|
296
|
+
if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
|
|
297
|
+
yield from self.extract_text_from_archive(file_name, data, label_prefix)
|
|
298
|
+
else:
|
|
299
|
+
if self.archive_should_skip(
|
|
300
|
+
file_name,
|
|
301
|
+
'content',
|
|
302
|
+
not info.is_directory,
|
|
303
|
+
info.is_directory,
|
|
304
|
+
get_archive_path_size(info, '7z')
|
|
305
|
+
):
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
text = try_decode(data)
|
|
309
|
+
if text is not None:
|
|
310
|
+
yield label_prefix + str(file_name), text
|
|
311
|
+
# Handle TAR and compressed TAR formats
|
|
256
312
|
elif file_ext in ('tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
|
|
257
|
-
mode =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
with tarfile.open(
|
|
313
|
+
mode = {
|
|
314
|
+
'tar': 'r',
|
|
315
|
+
'tar.gz': 'r:gz',
|
|
316
|
+
'tar.bz2': 'r:bz2',
|
|
317
|
+
'tar.xz': 'r:xz'
|
|
318
|
+
}[file_ext]
|
|
319
|
+
|
|
320
|
+
with tarfile.open(fileobj=file_stream, mode=mode) as tf:
|
|
265
321
|
for member in tf.getmembers():
|
|
266
|
-
file_name = Path(member.name)
|
|
267
|
-
if self.archive_should_skip(
|
|
268
|
-
file_name,
|
|
269
|
-
'content',
|
|
270
|
-
member.isfile(),
|
|
271
|
-
member.isdir(),
|
|
272
|
-
get_archive_path_size(member, file_ext)
|
|
273
|
-
):
|
|
274
|
-
continue
|
|
275
|
-
|
|
276
322
|
f = tf.extractfile(member)
|
|
277
323
|
if f is None:
|
|
278
324
|
continue
|
|
325
|
+
|
|
326
|
+
file_name = Path(member.name)
|
|
279
327
|
data = f.read()
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
328
|
+
new_depth = None if self.depth is None else self.depth - 1
|
|
329
|
+
|
|
330
|
+
if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
|
|
331
|
+
yield from self.extract_text_from_archive(file_name, data, label_prefix)
|
|
332
|
+
else:
|
|
333
|
+
if self.archive_should_skip(
|
|
334
|
+
file_name,
|
|
335
|
+
'content',
|
|
336
|
+
member.isfile(),
|
|
337
|
+
member.isdir(),
|
|
338
|
+
get_archive_path_size(member, file_ext)
|
|
339
|
+
):
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
text = try_decode(data)
|
|
343
|
+
if text is not None:
|
|
344
|
+
yield label_prefix + str(file_name), text
|
|
345
|
+
# Handle single compressed files like .gz, .bz2, .xz
|
|
346
|
+
elif file_ext in ARCHIVE_EXTS[-3:]:
|
|
347
|
+
opener = {'gz': gzip.open, 'bz2': bz2.open, 'xz': lzma.open}[file_ext]
|
|
348
|
+
with opener(file_stream, 'rb') as f:
|
|
297
349
|
data = f.read()
|
|
298
350
|
text = try_decode(data)
|
|
299
351
|
if text is not None:
|
|
300
|
-
yield
|
|
352
|
+
yield file_ext, text
|
|
301
353
|
except Exception:
|
|
302
354
|
return
|
|
303
355
|
|
|
@@ -319,7 +371,7 @@ class Search:
|
|
|
319
371
|
|
|
320
372
|
# Choose parent path based on full_path flag
|
|
321
373
|
p_parent = p_resolved.parent if self.full_path else p.parent
|
|
322
|
-
p_ext =
|
|
374
|
+
p_ext = get_path_suffix(p_resolved)
|
|
323
375
|
|
|
324
376
|
if pattern.evaluate(p.name) and not (search_type == 'directory' and p_resolved.is_file()):
|
|
325
377
|
# Highlight matched query in the name
|
|
@@ -327,11 +379,11 @@ class Search:
|
|
|
327
379
|
matches.append(f'{p_parent}\\{highlighted_name}')
|
|
328
380
|
|
|
329
381
|
# Search for files and directories name inside archive files if archive is active
|
|
330
|
-
if self.archive and p_ext in
|
|
331
|
-
for name in self.extract_names_from_archive(p_resolved, search_type):
|
|
382
|
+
if self.archive and p_ext in ARCHIVE_EXTS[:-3]:
|
|
383
|
+
for label, name in self.extract_names_from_archive(p_resolved, search_type):
|
|
332
384
|
if pattern.evaluate(name.name):
|
|
333
385
|
highlighted_name = highlight_text(pattern, name.name, self.fuzzy)
|
|
334
|
-
matches.append(f'{p_parent}\\{p.name}
|
|
386
|
+
matches.append(f'{p_parent}\\{p.name}{label}{name.parent}\\{highlighted_name}')
|
|
335
387
|
else: # content search
|
|
336
388
|
# Use dictionary: key: file path (colored), value: list of line matches
|
|
337
389
|
matches = {} if not self.no_content else set()
|
|
@@ -355,13 +407,14 @@ class Search:
|
|
|
355
407
|
file_label = str(file_path.resolve()) if self.full_path else str(file_path)
|
|
356
408
|
|
|
357
409
|
# First, check if the file is an archive, extract it from the archive and perform a search
|
|
358
|
-
if self.archive and
|
|
410
|
+
if self.archive and get_path_suffix(file_path) in ARCHIVE_EXTS:
|
|
359
411
|
for fname, content in self.extract_text_from_archive(file_path):
|
|
360
412
|
if not pattern.evaluate(content) and not self.expr:
|
|
361
413
|
continue
|
|
362
414
|
|
|
363
|
-
# Change file_label for archive files
|
|
364
|
-
|
|
415
|
+
# Change file_label for archive files (zip, rar, 7z, tar)
|
|
416
|
+
if fname not in ARCHIVE_EXTS[-3:]:
|
|
417
|
+
file_label += fname
|
|
365
418
|
|
|
366
419
|
if self.no_content and not self.expr:
|
|
367
420
|
matches.add(click.style(file_label, fg='cyan'))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pseek
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.2
|
|
4
4
|
Summary: Pseek is a Python library to search files, folders, and text
|
|
5
5
|
Author-email: Arian <ariannasiri86@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -102,6 +102,7 @@ pseek "error\d+" --regex
|
|
|
102
102
|
| `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
|
|
103
103
|
| `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
|
|
104
104
|
| `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
|
|
105
|
+
| `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
|
|
105
106
|
| `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
|
|
106
107
|
| `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
|
|
107
108
|
| `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
|
|
@@ -112,4 +113,4 @@ pseek "error\d+" --regex
|
|
|
112
113
|
## Requirements
|
|
113
114
|
|
|
114
115
|
* Python 3.6 or higher
|
|
115
|
-
* `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
|
|
116
|
+
* `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|