megfile 3.1.1__py3-none-any.whl → 3.1.2__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.
- docs/conf.py +2 -4
- megfile/__init__.py +394 -203
- megfile/cli.py +258 -238
- megfile/config.py +25 -21
- megfile/errors.py +124 -114
- megfile/fs.py +174 -140
- megfile/fs_path.py +462 -354
- megfile/hdfs.py +133 -101
- megfile/hdfs_path.py +290 -236
- megfile/http.py +15 -14
- megfile/http_path.py +111 -107
- megfile/interfaces.py +70 -65
- megfile/lib/base_prefetch_reader.py +84 -65
- megfile/lib/combine_reader.py +12 -12
- megfile/lib/compare.py +17 -13
- megfile/lib/compat.py +1 -5
- megfile/lib/fnmatch.py +29 -30
- megfile/lib/glob.py +46 -54
- megfile/lib/hdfs_prefetch_reader.py +40 -25
- megfile/lib/hdfs_tools.py +1 -3
- megfile/lib/http_prefetch_reader.py +69 -46
- megfile/lib/joinpath.py +5 -5
- megfile/lib/lazy_handler.py +7 -3
- megfile/lib/s3_buffered_writer.py +58 -51
- megfile/lib/s3_cached_handler.py +13 -14
- megfile/lib/s3_limited_seekable_writer.py +37 -28
- megfile/lib/s3_memory_handler.py +34 -30
- megfile/lib/s3_pipe_handler.py +24 -25
- megfile/lib/s3_prefetch_reader.py +71 -52
- megfile/lib/s3_share_cache_reader.py +37 -24
- megfile/lib/shadow_handler.py +7 -3
- megfile/lib/stdio_handler.py +9 -8
- megfile/lib/url.py +3 -3
- megfile/pathlike.py +259 -228
- megfile/s3.py +220 -153
- megfile/s3_path.py +977 -802
- megfile/sftp.py +190 -156
- megfile/sftp_path.py +540 -450
- megfile/smart.py +397 -330
- megfile/smart_path.py +100 -105
- megfile/stdio.py +10 -9
- megfile/stdio_path.py +32 -35
- megfile/utils/__init__.py +73 -54
- megfile/utils/mutex.py +11 -14
- megfile/version.py +1 -1
- {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/METADATA +5 -8
- megfile-3.1.2.dist-info/RECORD +55 -0
- {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/WHEEL +1 -1
- scripts/convert_results_to_sarif.py +45 -78
- scripts/generate_file.py +140 -64
- megfile-3.1.1.dist-info/RECORD +0 -55
- {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/LICENSE +0 -0
- {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/LICENSE.pyre +0 -0
- {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/entry_points.txt +0 -0
- {megfile-3.1.1.dist-info → megfile-3.1.2.dist-info}/top_level.txt +0 -0
megfile/sftp_path.py
CHANGED
|
@@ -22,25 +22,25 @@ from megfile.lib.compare import is_same_file
|
|
|
22
22
|
from megfile.lib.compat import fspath
|
|
23
23
|
from megfile.lib.glob import FSFunc, iglob
|
|
24
24
|
from megfile.lib.joinpath import uri_join
|
|
25
|
-
from megfile.pathlike import
|
|
25
|
+
from megfile.pathlike import URIPath
|
|
26
26
|
from megfile.smart_path import SmartPath
|
|
27
27
|
from megfile.utils import calculate_md5, thread_local
|
|
28
28
|
|
|
29
29
|
_logger = get_logger(__name__)
|
|
30
30
|
|
|
31
31
|
__all__ = [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
"SftpPath",
|
|
33
|
+
"is_sftp",
|
|
34
|
+
"sftp_readlink",
|
|
35
|
+
"sftp_glob",
|
|
36
|
+
"sftp_iglob",
|
|
37
|
+
"sftp_glob_stat",
|
|
38
|
+
"sftp_resolve",
|
|
39
|
+
"sftp_download",
|
|
40
|
+
"sftp_upload",
|
|
41
|
+
"sftp_path_join",
|
|
42
|
+
"sftp_concat",
|
|
43
|
+
"sftp_lstat",
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
SFTP_USERNAME = "SFTP_USERNAME"
|
|
@@ -66,19 +66,21 @@ def _make_stat(stat: paramiko.SFTPAttributes) -> StatResult:
|
|
|
66
66
|
|
|
67
67
|
def get_private_key():
|
|
68
68
|
key_with_types = {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
"DSA": paramiko.DSSKey,
|
|
70
|
+
"RSA": paramiko.RSAKey,
|
|
71
|
+
"ECDSA": paramiko.ECDSAKey,
|
|
72
|
+
"ED25519": paramiko.Ed25519Key,
|
|
73
73
|
}
|
|
74
|
-
key_type = os.getenv(SFTP_PRIVATE_KEY_TYPE,
|
|
74
|
+
key_type = os.getenv(SFTP_PRIVATE_KEY_TYPE, "RSA").upper()
|
|
75
75
|
if os.getenv(SFTP_PRIVATE_KEY_PATH):
|
|
76
76
|
private_key_path = os.getenv(SFTP_PRIVATE_KEY_PATH)
|
|
77
77
|
if not os.path.exists(private_key_path):
|
|
78
78
|
raise FileNotFoundError(
|
|
79
|
-
f"Private key file not exist: '{SFTP_PRIVATE_KEY_PATH}'"
|
|
79
|
+
f"Private key file not exist: '{SFTP_PRIVATE_KEY_PATH}'"
|
|
80
|
+
)
|
|
80
81
|
return key_with_types[key_type].from_private_key_file(
|
|
81
|
-
private_key_path, password=os.getenv(SFTP_PRIVATE_KEY_PASSWORD)
|
|
82
|
+
private_key_path, password=os.getenv(SFTP_PRIVATE_KEY_PASSWORD)
|
|
83
|
+
)
|
|
82
84
|
return None
|
|
83
85
|
|
|
84
86
|
|
|
@@ -101,17 +103,12 @@ def provide_connect_info(
|
|
|
101
103
|
def sftp_should_retry(error: Exception) -> bool:
|
|
102
104
|
if type(error) is EOFError:
|
|
103
105
|
return False
|
|
104
|
-
elif isinstance(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
socket.timeout,
|
|
108
|
-
)):
|
|
106
|
+
elif isinstance(
|
|
107
|
+
error, (paramiko.ssh_exception.SSHException, ConnectionError, socket.timeout)
|
|
108
|
+
):
|
|
109
109
|
return True
|
|
110
110
|
elif isinstance(error, OSError):
|
|
111
|
-
for err_msg in [
|
|
112
|
-
'Socket is closed',
|
|
113
|
-
'Cannot assign requested address',
|
|
114
|
-
]:
|
|
111
|
+
for err_msg in ["Socket is closed", "Cannot assign requested address"]:
|
|
115
112
|
if err_msg in str(error):
|
|
116
113
|
return True
|
|
117
114
|
return False
|
|
@@ -124,24 +121,20 @@ def _patch_sftp_client_request(
|
|
|
124
121
|
username: Optional[str] = None,
|
|
125
122
|
password: Optional[str] = None,
|
|
126
123
|
):
|
|
127
|
-
|
|
128
124
|
def retry_callback(error, *args, **kwargs):
|
|
129
125
|
client.close()
|
|
130
126
|
ssh_client = get_ssh_client(hostname, port, username, password)
|
|
131
127
|
ssh_client.close()
|
|
132
128
|
atexit.unregister(ssh_client.close)
|
|
133
|
-
ssh_key = f
|
|
129
|
+
ssh_key = f"ssh_client:{hostname},{port},{username},{password}"
|
|
134
130
|
if thread_local.get(ssh_key):
|
|
135
131
|
del thread_local[ssh_key]
|
|
136
|
-
sftp_key = f
|
|
132
|
+
sftp_key = f"sftp_client:{hostname},{port},{username},{password}"
|
|
137
133
|
if thread_local.get(sftp_key):
|
|
138
134
|
del thread_local[sftp_key]
|
|
139
135
|
|
|
140
136
|
new_sftp_client = get_sftp_client(
|
|
141
|
-
hostname=hostname,
|
|
142
|
-
port=port,
|
|
143
|
-
username=username,
|
|
144
|
-
password=password,
|
|
137
|
+
hostname=hostname, port=port, username=username, password=password
|
|
145
138
|
)
|
|
146
139
|
client.sock = new_sftp_client.sock
|
|
147
140
|
|
|
@@ -149,7 +142,8 @@ def _patch_sftp_client_request(
|
|
|
149
142
|
client._request, # pytype: disable=attribute-error
|
|
150
143
|
max_retries=MAX_RETRIES,
|
|
151
144
|
should_retry=sftp_should_retry,
|
|
152
|
-
retry_callback=retry_callback
|
|
145
|
+
retry_callback=retry_callback,
|
|
146
|
+
)
|
|
153
147
|
return client
|
|
154
148
|
|
|
155
149
|
|
|
@@ -159,15 +153,12 @@ def _get_sftp_client(
|
|
|
159
153
|
username: Optional[str] = None,
|
|
160
154
|
password: Optional[str] = None,
|
|
161
155
|
) -> paramiko.SFTPClient:
|
|
162
|
-
|
|
156
|
+
"""Get sftp client
|
|
163
157
|
|
|
164
158
|
:returns: sftp client
|
|
165
|
-
|
|
159
|
+
"""
|
|
166
160
|
session = get_ssh_session(
|
|
167
|
-
hostname=hostname,
|
|
168
|
-
port=port,
|
|
169
|
-
username=username,
|
|
170
|
-
password=password,
|
|
161
|
+
hostname=hostname, port=port, username=username, password=password
|
|
171
162
|
)
|
|
172
163
|
session.invoke_subsystem("sftp")
|
|
173
164
|
sftp_client = paramiko.SFTPClient(session)
|
|
@@ -181,13 +172,18 @@ def get_sftp_client(
|
|
|
181
172
|
username: Optional[str] = None,
|
|
182
173
|
password: Optional[str] = None,
|
|
183
174
|
) -> paramiko.SFTPClient:
|
|
184
|
-
|
|
175
|
+
"""Get sftp client
|
|
185
176
|
|
|
186
177
|
:returns: sftp client
|
|
187
|
-
|
|
178
|
+
"""
|
|
188
179
|
return thread_local(
|
|
189
|
-
f
|
|
190
|
-
_get_sftp_client,
|
|
180
|
+
f"sftp_client:{hostname},{port},{username},{password}",
|
|
181
|
+
_get_sftp_client,
|
|
182
|
+
hostname,
|
|
183
|
+
port,
|
|
184
|
+
username,
|
|
185
|
+
password,
|
|
186
|
+
)
|
|
191
187
|
|
|
192
188
|
|
|
193
189
|
def _get_ssh_client(
|
|
@@ -197,10 +193,7 @@ def _get_ssh_client(
|
|
|
197
193
|
password: Optional[str] = None,
|
|
198
194
|
) -> paramiko.SSHClient:
|
|
199
195
|
hostname, port, username, password, private_key = provide_connect_info(
|
|
200
|
-
hostname=hostname,
|
|
201
|
-
port=port,
|
|
202
|
-
username=username,
|
|
203
|
-
password=password,
|
|
196
|
+
hostname=hostname, port=port, username=username, password=password
|
|
204
197
|
)
|
|
205
198
|
|
|
206
199
|
ssh_client = paramiko.SSHClient()
|
|
@@ -209,12 +202,15 @@ def _get_ssh_client(
|
|
|
209
202
|
try:
|
|
210
203
|
fd = os.open(
|
|
211
204
|
os.path.join(
|
|
212
|
-
|
|
213
|
-
f
|
|
214
|
-
),
|
|
205
|
+
"/tmp",
|
|
206
|
+
f"megfile-sftp-{hostname}-{random.randint(1, max_unauth_connections)}",
|
|
207
|
+
),
|
|
208
|
+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
|
|
209
|
+
)
|
|
215
210
|
except Exception:
|
|
216
211
|
_logger.warning(
|
|
217
|
-
"Can't create file lock in '/tmp',
|
|
212
|
+
"Can't create file lock in '/tmp', "
|
|
213
|
+
"please control the SFTP concurrency count by yourself."
|
|
218
214
|
)
|
|
219
215
|
fd = None
|
|
220
216
|
if fd:
|
|
@@ -243,8 +239,13 @@ def get_ssh_client(
|
|
|
243
239
|
password: Optional[str] = None,
|
|
244
240
|
) -> paramiko.SSHClient:
|
|
245
241
|
return thread_local(
|
|
246
|
-
f
|
|
247
|
-
|
|
242
|
+
f"ssh_client:{hostname},{port},{username},{password}",
|
|
243
|
+
_get_ssh_client,
|
|
244
|
+
hostname,
|
|
245
|
+
port,
|
|
246
|
+
username,
|
|
247
|
+
password,
|
|
248
|
+
)
|
|
248
249
|
|
|
249
250
|
|
|
250
251
|
def get_ssh_session(
|
|
@@ -253,15 +254,14 @@ def get_ssh_session(
|
|
|
253
254
|
username: Optional[str] = None,
|
|
254
255
|
password: Optional[str] = None,
|
|
255
256
|
) -> paramiko.Channel:
|
|
256
|
-
|
|
257
257
|
def retry_callback(error, *args, **kwargs):
|
|
258
258
|
ssh_client = get_ssh_client(hostname, port, username, password)
|
|
259
259
|
ssh_client.close()
|
|
260
260
|
atexit.unregister(ssh_client.close)
|
|
261
|
-
ssh_key = f
|
|
261
|
+
ssh_key = f"ssh_client:{hostname},{port},{username},{password}"
|
|
262
262
|
if thread_local.get(ssh_key):
|
|
263
263
|
del thread_local[ssh_key]
|
|
264
|
-
sftp_key = f
|
|
264
|
+
sftp_key = f"sftp_client:{hostname},{port},{username},{password}"
|
|
265
265
|
if thread_local.get(sftp_key):
|
|
266
266
|
del thread_local[sftp_key]
|
|
267
267
|
|
|
@@ -269,12 +269,8 @@ def get_ssh_session(
|
|
|
269
269
|
_open_session,
|
|
270
270
|
max_retries=MAX_RETRIES,
|
|
271
271
|
should_retry=sftp_should_retry,
|
|
272
|
-
retry_callback=retry_callback
|
|
273
|
-
|
|
274
|
-
port,
|
|
275
|
-
username,
|
|
276
|
-
password,
|
|
277
|
-
)
|
|
272
|
+
retry_callback=retry_callback,
|
|
273
|
+
)(hostname, port, username, password)
|
|
278
274
|
|
|
279
275
|
|
|
280
276
|
def _open_session(
|
|
@@ -286,158 +282,187 @@ def _open_session(
|
|
|
286
282
|
ssh_client = get_ssh_client(hostname, port, username, password)
|
|
287
283
|
transport = ssh_client.get_transport()
|
|
288
284
|
if not transport:
|
|
289
|
-
raise paramiko.SSHException(
|
|
285
|
+
raise paramiko.SSHException("Get transport error")
|
|
290
286
|
transport.set_keepalive(DEFAULT_SSH_KEEPALIVE_INTERVAL)
|
|
291
287
|
session = transport.open_session(timeout=DEFAULT_SSH_CONNECT_TIMEOUT)
|
|
292
288
|
if not session:
|
|
293
|
-
raise paramiko.SSHException(
|
|
289
|
+
raise paramiko.SSHException("Create session error")
|
|
294
290
|
session.settimeout(DEFAULT_SSH_CONNECT_TIMEOUT)
|
|
295
291
|
return session
|
|
296
292
|
|
|
297
293
|
|
|
298
294
|
def is_sftp(path: PathLike) -> bool:
|
|
299
|
-
|
|
295
|
+
"""Test if a path is sftp path
|
|
300
296
|
|
|
301
297
|
:param path: Path to be tested
|
|
302
298
|
:returns: True of a path is sftp path, else False
|
|
303
|
-
|
|
299
|
+
"""
|
|
304
300
|
path = fspath(path)
|
|
305
301
|
parts = urlsplit(path)
|
|
306
|
-
return parts.scheme ==
|
|
302
|
+
return parts.scheme == "sftp"
|
|
307
303
|
|
|
308
304
|
|
|
309
|
-
def sftp_readlink(path: PathLike) ->
|
|
310
|
-
|
|
305
|
+
def sftp_readlink(path: PathLike) -> "str":
|
|
306
|
+
"""
|
|
311
307
|
Return a SftpPath instance representing the path to which the symbolic link points.
|
|
308
|
+
|
|
312
309
|
:param path: Given path
|
|
313
|
-
:returns: Return a SftpPath instance representing the path to
|
|
314
|
-
|
|
310
|
+
:returns: Return a SftpPath instance representing the path to
|
|
311
|
+
which the symbolic link points.
|
|
312
|
+
"""
|
|
315
313
|
return SftpPath(path).readlink().path_with_protocol
|
|
316
314
|
|
|
317
315
|
|
|
318
|
-
def sftp_glob(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
316
|
+
def sftp_glob(
|
|
317
|
+
path: PathLike, recursive: bool = True, missing_ok: bool = True
|
|
318
|
+
) -> List[str]:
|
|
319
|
+
"""Return path list in ascending alphabetical order,
|
|
320
|
+
in which path matches glob pattern
|
|
322
321
|
|
|
323
322
|
1. If doesn't match any path, return empty list
|
|
324
|
-
|
|
323
|
+
Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list
|
|
324
|
+
when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist.
|
|
325
|
+
fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
|
|
325
326
|
2. No guarantee that each path in result is different, which means:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
Assume there exists a path `/a/b/c/b/d.txt`
|
|
328
|
+
use path pattern like `/**/b/**/*.txt` to glob,
|
|
329
|
+
the path above will be returned twice
|
|
330
|
+
3. `**` will match any matched file, directory, symlink and '' by default,
|
|
331
|
+
when recursive is `True`
|
|
332
|
+
4. fs_glob returns same as glob.glob(pathname, recursive=True)
|
|
333
|
+
in ascending alphabetical order.
|
|
330
334
|
5. Hidden files (filename stars with '.') will not be found in the result
|
|
331
335
|
|
|
332
336
|
:param path: Given path
|
|
333
|
-
:param pattern: Glob the given relative pattern in the directory represented
|
|
337
|
+
:param pattern: Glob the given relative pattern in the directory represented
|
|
338
|
+
by this path
|
|
334
339
|
:param recursive: If False, `**` will not search directory recursively
|
|
335
|
-
:param missing_ok: If False and target path doesn't match any file,
|
|
340
|
+
:param missing_ok: If False and target path doesn't match any file,
|
|
341
|
+
raise FileNotFoundError
|
|
336
342
|
:returns: A list contains paths match `pathname`
|
|
337
|
-
|
|
338
|
-
return list(
|
|
339
|
-
sftp_iglob(path=path, recursive=recursive, missing_ok=missing_ok))
|
|
343
|
+
"""
|
|
344
|
+
return list(sftp_iglob(path=path, recursive=recursive, missing_ok=missing_ok))
|
|
340
345
|
|
|
341
346
|
|
|
342
347
|
def sftp_glob_stat(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
path: PathLike, recursive: bool = True, missing_ok: bool = True
|
|
349
|
+
) -> Iterator[FileEntry]:
|
|
350
|
+
"""Return a list contains tuples of path and file stat, in ascending alphabetical
|
|
351
|
+
order, in which path matches glob pattern
|
|
347
352
|
|
|
348
353
|
1. If doesn't match any path, return empty list
|
|
349
|
-
|
|
354
|
+
Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list
|
|
355
|
+
when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist.
|
|
356
|
+
sftp_glob behaves like ``glob.glob`` in standard library under such circumstance.
|
|
350
357
|
2. No guarantee that each path in result is different, which means:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
358
|
+
Assume there exists a path `/a/b/c/b/d.txt`
|
|
359
|
+
use path pattern like `/**/b/**/*.txt` to glob,
|
|
360
|
+
the path above will be returned twice
|
|
361
|
+
3. `**` will match any matched file, directory, symlink and '' by default,
|
|
362
|
+
when recursive is `True`
|
|
363
|
+
4. fs_glob returns same as glob.glob(pathname, recursive=True) in
|
|
364
|
+
ascending alphabetical order.
|
|
355
365
|
5. Hidden files (filename stars with '.') will not be found in the result
|
|
356
366
|
|
|
357
367
|
:param path: Given path
|
|
358
|
-
:param pattern: Glob the given relative pattern in the directory represented
|
|
368
|
+
:param pattern: Glob the given relative pattern in the directory represented
|
|
369
|
+
by this path
|
|
359
370
|
:param recursive: If False, `**` will not search directory recursively
|
|
360
|
-
:param missing_ok: If False and target path doesn't match any file,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
371
|
+
:param missing_ok: If False and target path doesn't match any file,
|
|
372
|
+
raise FileNotFoundError
|
|
373
|
+
:returns: A list contains tuples of path and file stat,
|
|
374
|
+
in which paths match `pathname`
|
|
375
|
+
"""
|
|
376
|
+
for path in sftp_iglob(path=path, recursive=recursive, missing_ok=missing_ok):
|
|
365
377
|
path_object = SftpPath(path)
|
|
366
378
|
yield FileEntry(
|
|
367
|
-
path_object.name, path_object.path_with_protocol,
|
|
368
|
-
|
|
379
|
+
path_object.name, path_object.path_with_protocol, path_object.lstat()
|
|
380
|
+
)
|
|
369
381
|
|
|
370
382
|
|
|
371
|
-
def sftp_iglob(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
383
|
+
def sftp_iglob(
|
|
384
|
+
path: PathLike, recursive: bool = True, missing_ok: bool = True
|
|
385
|
+
) -> Iterator[str]:
|
|
386
|
+
"""Return path iterator in ascending alphabetical order,
|
|
387
|
+
in which path matches glob pattern
|
|
375
388
|
|
|
376
389
|
1. If doesn't match any path, return empty list
|
|
377
|
-
|
|
390
|
+
Notice: ``glob.glob`` in standard library returns ['a/'] instead of empty list
|
|
391
|
+
when pathname is like `a/**`, recursive is True and directory 'a' doesn't exist.
|
|
392
|
+
fs_glob behaves like ``glob.glob`` in standard library under such circumstance.
|
|
378
393
|
2. No guarantee that each path in result is different, which means:
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
394
|
+
Assume there exists a path `/a/b/c/b/d.txt`
|
|
395
|
+
use path pattern like `/**/b/**/*.txt` to glob,
|
|
396
|
+
the path above will be returned twice
|
|
397
|
+
3. `**` will match any matched file, directory, symlink and '' by default,
|
|
398
|
+
when recursive is `True`
|
|
399
|
+
4. fs_glob returns same as glob.glob(pathname, recursive=True) in
|
|
400
|
+
ascending alphabetical order.
|
|
383
401
|
5. Hidden files (filename stars with '.') will not be found in the result
|
|
384
402
|
|
|
385
403
|
:param path: Given path
|
|
386
|
-
:param pattern: Glob the given relative pattern in the directory represented
|
|
404
|
+
:param pattern: Glob the given relative pattern in the directory represented
|
|
405
|
+
by this path
|
|
387
406
|
:param recursive: If False, `**` will not search directory recursively
|
|
388
|
-
:param missing_ok: If False and target path doesn't match any file,
|
|
407
|
+
:param missing_ok: If False and target path doesn't match any file,
|
|
408
|
+
raise FileNotFoundError
|
|
389
409
|
:returns: An iterator contains paths match `pathname`
|
|
390
|
-
|
|
410
|
+
"""
|
|
391
411
|
|
|
392
|
-
for path in SftpPath(path).iglob(
|
|
393
|
-
|
|
412
|
+
for path in SftpPath(path).iglob(
|
|
413
|
+
pattern="", recursive=recursive, missing_ok=missing_ok
|
|
414
|
+
):
|
|
394
415
|
yield path.path_with_protocol
|
|
395
416
|
|
|
396
417
|
|
|
397
|
-
def sftp_resolve(path: PathLike, strict=False) ->
|
|
398
|
-
|
|
418
|
+
def sftp_resolve(path: PathLike, strict=False) -> "str":
|
|
419
|
+
"""Equal to fs_realpath
|
|
399
420
|
|
|
400
421
|
:param path: Given path
|
|
401
422
|
:param strict: Ignore this parameter, just for compatibility
|
|
402
|
-
:return: Return the canonical path of the specified filename,
|
|
423
|
+
:return: Return the canonical path of the specified filename,
|
|
424
|
+
eliminating any symbolic links encountered in the path.
|
|
403
425
|
:rtype: SftpPath
|
|
404
|
-
|
|
426
|
+
"""
|
|
405
427
|
return SftpPath(path).resolve(strict).path_with_protocol
|
|
406
428
|
|
|
407
429
|
|
|
408
|
-
def _sftp_scan_pairs(
|
|
409
|
-
|
|
430
|
+
def _sftp_scan_pairs(
|
|
431
|
+
src_url: PathLike, dst_url: PathLike
|
|
432
|
+
) -> Iterator[Tuple[PathLike, PathLike]]:
|
|
410
433
|
for src_file_path in SftpPath(src_url).scan():
|
|
411
|
-
content_path = src_file_path[len(src_url):]
|
|
434
|
+
content_path = src_file_path[len(src_url) :]
|
|
412
435
|
if len(content_path) > 0:
|
|
413
|
-
dst_file_path = SftpPath(dst_url).joinpath(
|
|
414
|
-
content_path).path_with_protocol
|
|
436
|
+
dst_file_path = SftpPath(dst_url).joinpath(content_path).path_with_protocol
|
|
415
437
|
else:
|
|
416
438
|
dst_file_path = dst_url
|
|
417
439
|
yield src_file_path, dst_file_path
|
|
418
440
|
|
|
419
441
|
|
|
420
442
|
def sftp_download(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
443
|
+
src_url: PathLike,
|
|
444
|
+
dst_url: PathLike,
|
|
445
|
+
callback: Optional[Callable[[int], None]] = None,
|
|
446
|
+
followlinks: bool = False,
|
|
447
|
+
overwrite: bool = True,
|
|
448
|
+
):
|
|
449
|
+
"""
|
|
427
450
|
Downloads a file from sftp to local filesystem.
|
|
451
|
+
|
|
428
452
|
:param src_url: source sftp path
|
|
429
453
|
:param dst_url: target fs path
|
|
430
|
-
:param callback: Called periodically during copy, and the input parameter is
|
|
454
|
+
:param callback: Called periodically during copy, and the input parameter is
|
|
455
|
+
the data size (in bytes) of copy since the last call
|
|
431
456
|
:param followlinks: False if regard symlink as file, else True
|
|
432
457
|
:param overwrite: whether or not overwrite file when exists, default is True
|
|
433
|
-
|
|
458
|
+
"""
|
|
434
459
|
from megfile.fs import is_fs
|
|
435
460
|
from megfile.fs_path import FSPath
|
|
436
461
|
|
|
437
462
|
if not is_fs(dst_url):
|
|
438
|
-
raise OSError(f
|
|
463
|
+
raise OSError(f"dst_url is not fs path: {dst_url}")
|
|
439
464
|
if not is_sftp(src_url) and not isinstance(src_url, SftpPath):
|
|
440
|
-
raise OSError(f
|
|
465
|
+
raise OSError(f"src_url is not sftp path: {src_url}")
|
|
441
466
|
|
|
442
467
|
dst_path = FSPath(dst_url)
|
|
443
468
|
if not overwrite and dst_path.exists():
|
|
@@ -451,9 +476,9 @@ def sftp_download(
|
|
|
451
476
|
if followlinks and src_path.is_symlink():
|
|
452
477
|
src_path = src_path.readlink()
|
|
453
478
|
if src_path.is_dir():
|
|
454
|
-
raise IsADirectoryError(
|
|
455
|
-
if str(dst_url).endswith(
|
|
456
|
-
raise IsADirectoryError(
|
|
479
|
+
raise IsADirectoryError("Is a directory: %r" % src_url)
|
|
480
|
+
if str(dst_url).endswith("/"):
|
|
481
|
+
raise IsADirectoryError("Is a directory: %r" % dst_url)
|
|
457
482
|
|
|
458
483
|
dst_path.parent.makedirs(exist_ok=True)
|
|
459
484
|
|
|
@@ -463,14 +488,12 @@ def sftp_download(
|
|
|
463
488
|
|
|
464
489
|
def sftp_callback(bytes_transferred: int, _total_bytes: int):
|
|
465
490
|
nonlocal bytes_transferred_before
|
|
466
|
-
callback( # pyre-ignore[29]
|
|
467
|
-
bytes_transferred - bytes_transferred_before)
|
|
491
|
+
callback(bytes_transferred - bytes_transferred_before) # pyre-ignore[29]
|
|
468
492
|
bytes_transferred_before = bytes_transferred
|
|
469
493
|
|
|
470
494
|
src_path._client.get(
|
|
471
|
-
src_path._real_path,
|
|
472
|
-
|
|
473
|
-
callback=sftp_callback)
|
|
495
|
+
src_path._real_path, dst_path.path_without_protocol, callback=sftp_callback
|
|
496
|
+
)
|
|
474
497
|
|
|
475
498
|
src_stat = src_path.stat()
|
|
476
499
|
dst_path.utime(src_stat.st_atime, src_stat.st_mtime)
|
|
@@ -478,32 +501,35 @@ def sftp_download(
|
|
|
478
501
|
|
|
479
502
|
|
|
480
503
|
def sftp_upload(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
504
|
+
src_url: PathLike,
|
|
505
|
+
dst_url: PathLike,
|
|
506
|
+
callback: Optional[Callable[[int], None]] = None,
|
|
507
|
+
followlinks: bool = False,
|
|
508
|
+
overwrite: bool = True,
|
|
509
|
+
):
|
|
510
|
+
"""
|
|
487
511
|
Uploads a file from local filesystem to sftp server.
|
|
512
|
+
|
|
488
513
|
:param src_url: source fs path
|
|
489
514
|
:param dst_url: target sftp path
|
|
490
|
-
:param callback: Called periodically during copy, and the input parameter is
|
|
515
|
+
:param callback: Called periodically during copy, and the input parameter is
|
|
516
|
+
the data size (in bytes) of copy since the last call
|
|
491
517
|
:param overwrite: whether or not overwrite file when exists, default is True
|
|
492
|
-
|
|
518
|
+
"""
|
|
493
519
|
from megfile.fs import is_fs
|
|
494
520
|
from megfile.fs_path import FSPath
|
|
495
521
|
|
|
496
522
|
if not is_fs(src_url):
|
|
497
|
-
raise OSError(f
|
|
523
|
+
raise OSError(f"src_url is not fs path: {src_url}")
|
|
498
524
|
if not is_sftp(dst_url) and not isinstance(dst_url, SftpPath):
|
|
499
|
-
raise OSError(f
|
|
525
|
+
raise OSError(f"dst_url is not sftp path: {dst_url}")
|
|
500
526
|
|
|
501
527
|
if followlinks and os.path.islink(src_url):
|
|
502
528
|
src_url = os.readlink(src_url)
|
|
503
529
|
if os.path.isdir(src_url):
|
|
504
|
-
raise IsADirectoryError(
|
|
505
|
-
if str(dst_url).endswith(
|
|
506
|
-
raise IsADirectoryError(
|
|
530
|
+
raise IsADirectoryError("Is a directory: %r" % src_url)
|
|
531
|
+
if str(dst_url).endswith("/"):
|
|
532
|
+
raise IsADirectoryError("Is a directory: %r" % dst_url)
|
|
507
533
|
|
|
508
534
|
src_path = FSPath(src_url)
|
|
509
535
|
if isinstance(dst_url, SftpPath):
|
|
@@ -521,14 +547,12 @@ def sftp_upload(
|
|
|
521
547
|
|
|
522
548
|
def sftp_callback(bytes_transferred: int, _total_bytes: int):
|
|
523
549
|
nonlocal bytes_transferred_before
|
|
524
|
-
callback( # pyre-ignore[29]
|
|
525
|
-
bytes_transferred - bytes_transferred_before)
|
|
550
|
+
callback(bytes_transferred - bytes_transferred_before) # pyre-ignore[29]
|
|
526
551
|
bytes_transferred_before = bytes_transferred
|
|
527
552
|
|
|
528
553
|
dst_path._client.put(
|
|
529
|
-
src_path.path_without_protocol,
|
|
530
|
-
|
|
531
|
-
callback=sftp_callback)
|
|
554
|
+
src_path.path_without_protocol, dst_path._real_path, callback=sftp_callback
|
|
555
|
+
)
|
|
532
556
|
|
|
533
557
|
src_stat = src_path.stat()
|
|
534
558
|
dst_path.utime(src_stat.st_atime, src_stat.st_mtime)
|
|
@@ -536,7 +560,7 @@ def sftp_upload(
|
|
|
536
560
|
|
|
537
561
|
|
|
538
562
|
def sftp_path_join(path: PathLike, *other_paths: PathLike) -> str:
|
|
539
|
-
|
|
563
|
+
"""
|
|
540
564
|
Concat 2 or more path to a complete path
|
|
541
565
|
|
|
542
566
|
:param path: Given path
|
|
@@ -545,40 +569,42 @@ def sftp_path_join(path: PathLike, *other_paths: PathLike) -> str:
|
|
|
545
569
|
|
|
546
570
|
.. note ::
|
|
547
571
|
|
|
548
|
-
The difference between this function and ``os.path.join`` is that this function
|
|
549
|
-
|
|
550
|
-
|
|
572
|
+
The difference between this function and ``os.path.join`` is that this function
|
|
573
|
+
ignores left side slash (which indicates absolute path) in ``other_paths``
|
|
574
|
+
and will directly concat.
|
|
575
|
+
|
|
576
|
+
e.g. os.path.join('/path', 'to', '/file') => '/file',
|
|
577
|
+
but sftp_path_join('/path', 'to', '/file') => '/path/to/file'
|
|
578
|
+
"""
|
|
551
579
|
return uri_join(fspath(path), *map(fspath, other_paths))
|
|
552
580
|
|
|
553
581
|
|
|
554
582
|
def sftp_concat(src_paths: List[PathLike], dst_path: PathLike) -> None:
|
|
555
|
-
|
|
583
|
+
"""Concatenate sftp files to one file.
|
|
556
584
|
|
|
557
585
|
:param src_paths: Given source paths
|
|
558
586
|
:param dst_path: Given destination path
|
|
559
|
-
|
|
587
|
+
"""
|
|
560
588
|
dst_path_obj = SftpPath(dst_path)
|
|
561
589
|
|
|
562
590
|
def get_real_path(path: PathLike) -> str:
|
|
563
591
|
return SftpPath(path)._real_path
|
|
564
592
|
|
|
565
|
-
command = [
|
|
566
|
-
'cat', *map(get_real_path, src_paths), '>',
|
|
567
|
-
get_real_path(dst_path)
|
|
568
|
-
]
|
|
593
|
+
command = ["cat", *map(get_real_path, src_paths), ">", get_real_path(dst_path)]
|
|
569
594
|
exec_result = dst_path_obj._exec_command(command)
|
|
570
595
|
if exec_result.returncode != 0:
|
|
571
596
|
_logger.error(exec_result.stderr)
|
|
572
|
-
raise OSError(f
|
|
597
|
+
raise OSError(f"Failed to concat {src_paths} to {dst_path}")
|
|
573
598
|
|
|
574
599
|
|
|
575
600
|
def sftp_lstat(path: PathLike) -> StatResult:
|
|
576
|
-
|
|
577
|
-
Get StatResult of file on sftp, including file size and mtime,
|
|
601
|
+
"""
|
|
602
|
+
Get StatResult of file on sftp, including file size and mtime,
|
|
603
|
+
referring to fs_getsize and fs_getmtime
|
|
578
604
|
|
|
579
605
|
:param path: Given path
|
|
580
606
|
:returns: StatResult
|
|
581
|
-
|
|
607
|
+
"""
|
|
582
608
|
return SftpPath(path).lstat()
|
|
583
609
|
|
|
584
610
|
|
|
@@ -586,7 +612,7 @@ def sftp_lstat(path: PathLike) -> StatResult:
|
|
|
586
612
|
class SftpPath(URIPath):
|
|
587
613
|
"""sftp protocol
|
|
588
614
|
|
|
589
|
-
uri format:
|
|
615
|
+
uri format:
|
|
590
616
|
- absolute path
|
|
591
617
|
- sftp://[username[:password]@]hostname[:port]//file_path
|
|
592
618
|
- relative path
|
|
@@ -600,23 +626,23 @@ class SftpPath(URIPath):
|
|
|
600
626
|
parts = urlsplit(self.path)
|
|
601
627
|
self._urlsplit_parts = parts
|
|
602
628
|
self._real_path = parts.path
|
|
603
|
-
if parts.path.startswith(
|
|
604
|
-
self._root_dir =
|
|
629
|
+
if parts.path.startswith("//"):
|
|
630
|
+
self._root_dir = "/"
|
|
605
631
|
else:
|
|
606
|
-
self._root_dir = self._client.normalize(
|
|
607
|
-
self._real_path = os.path.join(self._root_dir, parts.path.lstrip(
|
|
632
|
+
self._root_dir = self._client.normalize(".")
|
|
633
|
+
self._real_path = os.path.join(self._root_dir, parts.path.lstrip("/"))
|
|
608
634
|
|
|
609
635
|
@cached_property
|
|
610
636
|
def parts(self) -> Tuple[str, ...]:
|
|
611
|
-
|
|
612
|
-
if self._urlsplit_parts.path.startswith(
|
|
613
|
-
new_parts = self._urlsplit_parts._replace(path=
|
|
637
|
+
"""A tuple giving access to the path’s various components"""
|
|
638
|
+
if self._urlsplit_parts.path.startswith("//"):
|
|
639
|
+
new_parts = self._urlsplit_parts._replace(path="//")
|
|
614
640
|
else:
|
|
615
|
-
new_parts = self._urlsplit_parts._replace(path=
|
|
641
|
+
new_parts = self._urlsplit_parts._replace(path="/")
|
|
616
642
|
parts = [urlunsplit(new_parts)]
|
|
617
|
-
path = self._urlsplit_parts.path.lstrip(
|
|
618
|
-
if path !=
|
|
619
|
-
parts.extend(path.split(
|
|
643
|
+
path = self._urlsplit_parts.path.lstrip("/")
|
|
644
|
+
if path != "":
|
|
645
|
+
parts.extend(path.split("/"))
|
|
620
646
|
return tuple(parts) # pyre-ignore[7]
|
|
621
647
|
|
|
622
648
|
@property
|
|
@@ -625,28 +651,27 @@ class SftpPath(URIPath):
|
|
|
625
651
|
hostname=self._urlsplit_parts.hostname,
|
|
626
652
|
port=self._urlsplit_parts.port,
|
|
627
653
|
username=self._urlsplit_parts.username,
|
|
628
|
-
password=self._urlsplit_parts.password
|
|
654
|
+
password=self._urlsplit_parts.password,
|
|
655
|
+
)
|
|
629
656
|
|
|
630
|
-
def _generate_path_object(
|
|
631
|
-
|
|
632
|
-
if resolve or self._root_dir == '/':
|
|
657
|
+
def _generate_path_object(self, sftp_local_path: str, resolve: bool = False):
|
|
658
|
+
if resolve or self._root_dir == "/":
|
|
633
659
|
sftp_local_path = f"//{sftp_local_path.lstrip('/')}"
|
|
634
660
|
else:
|
|
635
|
-
sftp_local_path = os.path.relpath(
|
|
636
|
-
sftp_local_path, start=self._root_dir)
|
|
661
|
+
sftp_local_path = os.path.relpath(sftp_local_path, start=self._root_dir)
|
|
637
662
|
if sftp_local_path == ".":
|
|
638
663
|
sftp_local_path = "/"
|
|
639
664
|
new_parts = self._urlsplit_parts._replace(path=sftp_local_path)
|
|
640
665
|
return self.from_path(urlunsplit(new_parts)) # pyre-ignore[6]
|
|
641
666
|
|
|
642
667
|
def exists(self, followlinks: bool = False) -> bool:
|
|
643
|
-
|
|
668
|
+
"""
|
|
644
669
|
Test if the path exists
|
|
645
670
|
|
|
646
671
|
:param followlinks: False if regard symlink as file, else True
|
|
647
672
|
:returns: True if the path exists, else False
|
|
648
673
|
|
|
649
|
-
|
|
674
|
+
"""
|
|
650
675
|
try:
|
|
651
676
|
if followlinks:
|
|
652
677
|
self._client.stat(self._real_path)
|
|
@@ -657,94 +682,125 @@ class SftpPath(URIPath):
|
|
|
657
682
|
return False
|
|
658
683
|
|
|
659
684
|
def getmtime(self, follow_symlinks: bool = False) -> float:
|
|
660
|
-
|
|
685
|
+
"""
|
|
661
686
|
Get last-modified time of the file on the given path (in Unix timestamp format).
|
|
662
|
-
|
|
687
|
+
|
|
688
|
+
If the path is an existent directory,
|
|
689
|
+
return the latest modified time of all file in it.
|
|
663
690
|
|
|
664
691
|
:returns: last-modified time
|
|
665
|
-
|
|
692
|
+
"""
|
|
666
693
|
return self.stat(follow_symlinks=follow_symlinks).mtime
|
|
667
694
|
|
|
668
695
|
def getsize(self, follow_symlinks: bool = False) -> int:
|
|
669
|
-
|
|
696
|
+
"""
|
|
670
697
|
Get file size on the given file path (in bytes).
|
|
671
|
-
|
|
672
|
-
|
|
698
|
+
|
|
699
|
+
If the path in a directory, return the sum of all file size in it,
|
|
700
|
+
including file in subdirectories (if exist).
|
|
701
|
+
|
|
702
|
+
The result excludes the size of directory itself. In other words,
|
|
703
|
+
return 0 Byte on an empty directory path.
|
|
673
704
|
|
|
674
705
|
:returns: File size
|
|
675
706
|
|
|
676
|
-
|
|
707
|
+
"""
|
|
677
708
|
return self.stat(follow_symlinks=follow_symlinks).size
|
|
678
709
|
|
|
679
|
-
def glob(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
710
|
+
def glob(
|
|
711
|
+
self, pattern, recursive: bool = True, missing_ok: bool = True
|
|
712
|
+
) -> List["SftpPath"]:
|
|
713
|
+
"""Return path list in ascending alphabetical order,
|
|
714
|
+
in which path matches glob pattern
|
|
684
715
|
|
|
685
716
|
1. If doesn't match any path, return empty list
|
|
686
|
-
|
|
717
|
+
Notice: ``glob.glob`` in standard library returns ['a/'] instead of
|
|
718
|
+
empty list when pathname is like `a/**`, recursive is True and directory 'a'
|
|
719
|
+
doesn't exist. fs_glob behaves like ``glob.glob`` in standard library under
|
|
720
|
+
such circumstance.
|
|
687
721
|
2. No guarantee that each path in result is different, which means:
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
722
|
+
Assume there exists a path `/a/b/c/b/d.txt`
|
|
723
|
+
use path pattern like `/**/b/**/*.txt` to glob,
|
|
724
|
+
the path above will be returned twice
|
|
725
|
+
3. `**` will match any matched file, directory, symlink and '' by default,
|
|
726
|
+
when recursive is `True`
|
|
727
|
+
4. fs_glob returns same as glob.glob(pathname, recursive=True) in
|
|
728
|
+
ascending alphabetical order.
|
|
692
729
|
5. Hidden files (filename stars with '.') will not be found in the result
|
|
693
730
|
|
|
694
|
-
:param pattern: Glob the given relative pattern in the directory represented
|
|
731
|
+
:param pattern: Glob the given relative pattern in the directory represented
|
|
732
|
+
by this path
|
|
695
733
|
:param recursive: If False, `**` will not search directory recursively
|
|
696
|
-
:param missing_ok: If False and target path doesn't match any file,
|
|
734
|
+
:param missing_ok: If False and target path doesn't match any file,
|
|
735
|
+
raise FileNotFoundError
|
|
697
736
|
:returns: A list contains paths match `pathname`
|
|
698
|
-
|
|
737
|
+
"""
|
|
699
738
|
return list(
|
|
700
|
-
self.iglob(
|
|
701
|
-
|
|
739
|
+
self.iglob(pattern=pattern, recursive=recursive, missing_ok=missing_ok)
|
|
740
|
+
)
|
|
702
741
|
|
|
703
742
|
def glob_stat(
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
'''Return a list contains tuples of path and file stat, in ascending alphabetical order, in which path matches glob pattern
|
|
743
|
+
self, pattern, recursive: bool = True, missing_ok: bool = True
|
|
744
|
+
) -> Iterator[FileEntry]:
|
|
745
|
+
"""Return a list contains tuples of path and file stat,
|
|
746
|
+
in ascending alphabetical order, in which path matches glob pattern
|
|
709
747
|
|
|
710
748
|
1. If doesn't match any path, return empty list
|
|
711
|
-
|
|
749
|
+
Notice: ``glob.glob`` in standard library returns ['a/'] instead of
|
|
750
|
+
empty list when pathname is like `a/**`, recursive is True and
|
|
751
|
+
directory 'a' doesn't exist. sftp_glob behaves like ``glob.glob`` in
|
|
752
|
+
standard library under such circumstance.
|
|
712
753
|
2. No guarantee that each path in result is different, which means:
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
754
|
+
Assume there exists a path `/a/b/c/b/d.txt`
|
|
755
|
+
use path pattern like `/**/b/**/*.txt` to glob,
|
|
756
|
+
the path above will be returned twice
|
|
757
|
+
3. `**` will match any matched file, directory, symlink and '' by default,
|
|
758
|
+
when recursive is `True`
|
|
759
|
+
4. fs_glob returns same as glob.glob(pathname, recursive=True) in
|
|
760
|
+
ascending alphabetical order.
|
|
717
761
|
5. Hidden files (filename stars with '.') will not be found in the result
|
|
718
762
|
|
|
719
|
-
:param pattern: Glob the given relative pattern in the directory represented
|
|
763
|
+
:param pattern: Glob the given relative pattern in the directory represented
|
|
764
|
+
by this path
|
|
720
765
|
:param recursive: If False, `**` will not search directory recursively
|
|
721
|
-
:param missing_ok: If False and target path doesn't match any file,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
766
|
+
:param missing_ok: If False and target path doesn't match any file,
|
|
767
|
+
raise FileNotFoundError
|
|
768
|
+
:returns: A list contains tuples of path and file stat,
|
|
769
|
+
in which paths match `pathname`
|
|
770
|
+
"""
|
|
771
|
+
for path_obj in self.iglob(
|
|
772
|
+
pattern=pattern, recursive=recursive, missing_ok=missing_ok
|
|
773
|
+
):
|
|
726
774
|
yield FileEntry(path_obj.name, path_obj.path, path_obj.lstat())
|
|
727
775
|
|
|
728
|
-
def iglob(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
776
|
+
def iglob(
|
|
777
|
+
self, pattern, recursive: bool = True, missing_ok: bool = True
|
|
778
|
+
) -> Iterator["SftpPath"]:
|
|
779
|
+
"""Return path iterator in ascending alphabetical order,
|
|
780
|
+
in which path matches glob pattern
|
|
733
781
|
|
|
734
782
|
1. If doesn't match any path, return empty list
|
|
735
|
-
|
|
783
|
+
Notice: ``glob.glob`` in standard library returns ['a/'] instead of
|
|
784
|
+
empty list when pathname is like `a/**`, recursive is True and
|
|
785
|
+
directory 'a' doesn't exist. fs_glob behaves like ``glob.glob`` in
|
|
786
|
+
standard library under such circumstance.
|
|
736
787
|
2. No guarantee that each path in result is different, which means:
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
788
|
+
Assume there exists a path `/a/b/c/b/d.txt`
|
|
789
|
+
use path pattern like `/**/b/**/*.txt` to glob,
|
|
790
|
+
the path above will be returned twice
|
|
791
|
+
3. `**` will match any matched file, directory, symlink and '' by default,
|
|
792
|
+
when recursive is `True`
|
|
793
|
+
4. fs_glob returns same as glob.glob(pathname, recursive=True) in
|
|
794
|
+
ascending alphabetical order.
|
|
741
795
|
5. Hidden files (filename stars with '.') will not be found in the result
|
|
742
796
|
|
|
743
|
-
:param pattern: Glob the given relative pattern in the directory represented
|
|
797
|
+
:param pattern: Glob the given relative pattern in the directory represented
|
|
798
|
+
by this path
|
|
744
799
|
:param recursive: If False, `**` will not search directory recursively
|
|
745
|
-
:param missing_ok: If False and target path doesn't match any file,
|
|
800
|
+
:param missing_ok: If False and target path doesn't match any file,
|
|
801
|
+
raise FileNotFoundError
|
|
746
802
|
:returns: An iterator contains paths match `pathname`
|
|
747
|
-
|
|
803
|
+
"""
|
|
748
804
|
glob_path = self.path_with_protocol
|
|
749
805
|
if pattern:
|
|
750
806
|
glob_path = self.joinpath(pattern).path_with_protocol
|
|
@@ -761,23 +817,25 @@ class SftpPath(URIPath):
|
|
|
761
817
|
|
|
762
818
|
fs = FSFunc(_exist, _is_dir, _scandir)
|
|
763
819
|
for real_path in _create_missing_ok_generator(
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
820
|
+
iglob(fspath(glob_path), recursive=recursive, fs=fs),
|
|
821
|
+
missing_ok,
|
|
822
|
+
FileNotFoundError("No match any file: %r" % glob_path),
|
|
823
|
+
):
|
|
767
824
|
yield self.from_path(real_path)
|
|
768
825
|
|
|
769
826
|
def is_dir(self, followlinks: bool = False) -> bool:
|
|
770
|
-
|
|
827
|
+
"""
|
|
771
828
|
Test if a path is directory
|
|
772
829
|
|
|
773
830
|
.. note::
|
|
774
831
|
|
|
775
|
-
The difference between this function and ``os.path.isdir`` is that
|
|
832
|
+
The difference between this function and ``os.path.isdir`` is that
|
|
833
|
+
this function regard symlink as file
|
|
776
834
|
|
|
777
835
|
:param followlinks: False if regard symlink as file, else True
|
|
778
836
|
:returns: True if the path is a directory, else False
|
|
779
837
|
|
|
780
|
-
|
|
838
|
+
"""
|
|
781
839
|
try:
|
|
782
840
|
stat = self.stat(follow_symlinks=followlinks)
|
|
783
841
|
if S_ISDIR(stat.st_mode):
|
|
@@ -787,17 +845,18 @@ class SftpPath(URIPath):
|
|
|
787
845
|
return False
|
|
788
846
|
|
|
789
847
|
def is_file(self, followlinks: bool = False) -> bool:
|
|
790
|
-
|
|
848
|
+
"""
|
|
791
849
|
Test if a path is file
|
|
792
850
|
|
|
793
851
|
.. note::
|
|
794
|
-
|
|
795
|
-
The difference between this function and ``os.path.isfile`` is that
|
|
852
|
+
|
|
853
|
+
The difference between this function and ``os.path.isfile`` is that
|
|
854
|
+
this function regard symlink as file
|
|
796
855
|
|
|
797
856
|
:param followlinks: False if regard symlink as file, else True
|
|
798
857
|
:returns: True if the path is a file, else False
|
|
799
858
|
|
|
800
|
-
|
|
859
|
+
"""
|
|
801
860
|
try:
|
|
802
861
|
stat = self.stat(follow_symlinks=followlinks)
|
|
803
862
|
if S_ISREG(stat.st_mode):
|
|
@@ -807,55 +866,56 @@ class SftpPath(URIPath):
|
|
|
807
866
|
return False
|
|
808
867
|
|
|
809
868
|
def listdir(self) -> List[str]:
|
|
810
|
-
|
|
811
|
-
Get all contents of given sftp path.
|
|
869
|
+
"""
|
|
870
|
+
Get all contents of given sftp path.
|
|
871
|
+
The result is in ascending alphabetical order.
|
|
812
872
|
|
|
813
873
|
:returns: All contents have in the path in ascending alphabetical order
|
|
814
|
-
|
|
874
|
+
"""
|
|
815
875
|
if not self.is_dir():
|
|
816
|
-
raise NotADirectoryError(
|
|
817
|
-
f"Not a directory: '{self.path_with_protocol}'")
|
|
876
|
+
raise NotADirectoryError(f"Not a directory: '{self.path_with_protocol}'")
|
|
818
877
|
return sorted(self._client.listdir(self._real_path))
|
|
819
878
|
|
|
820
|
-
def iterdir(self) -> Iterator[
|
|
821
|
-
|
|
822
|
-
Get all contents of given sftp path.
|
|
879
|
+
def iterdir(self) -> Iterator["SftpPath"]:
|
|
880
|
+
"""
|
|
881
|
+
Get all contents of given sftp path.
|
|
882
|
+
The result is in ascending alphabetical order.
|
|
823
883
|
|
|
824
884
|
:returns: All contents have in the path in ascending alphabetical order
|
|
825
|
-
|
|
885
|
+
"""
|
|
826
886
|
if not self.is_dir():
|
|
827
|
-
raise NotADirectoryError(
|
|
828
|
-
f"Not a directory: '{self.path_with_protocol}'")
|
|
887
|
+
raise NotADirectoryError(f"Not a directory: '{self.path_with_protocol}'")
|
|
829
888
|
for path in self.listdir():
|
|
830
889
|
yield self.joinpath(path)
|
|
831
890
|
|
|
832
891
|
def load(self) -> BinaryIO:
|
|
833
|
-
|
|
892
|
+
"""Read all content on specified path and write into memory
|
|
834
893
|
|
|
835
894
|
User should close the BinaryIO manually
|
|
836
895
|
|
|
837
896
|
:returns: Binary stream
|
|
838
|
-
|
|
839
|
-
with self.open(mode=
|
|
897
|
+
"""
|
|
898
|
+
with self.open(mode="rb") as f:
|
|
840
899
|
data = f.read()
|
|
841
900
|
return io.BytesIO(data)
|
|
842
901
|
|
|
843
902
|
def mkdir(self, mode=0o777, parents: bool = False, exist_ok: bool = False):
|
|
844
|
-
|
|
903
|
+
"""
|
|
845
904
|
make a directory on sftp, including parent directory.
|
|
846
905
|
If there exists a file on the path, raise FileExistsError
|
|
847
906
|
|
|
848
|
-
:param mode: If mode is given, it is combined with the process’ umask value to
|
|
849
|
-
|
|
850
|
-
|
|
907
|
+
:param mode: If mode is given, it is combined with the process’ umask value to
|
|
908
|
+
determine the file mode and access flags.
|
|
909
|
+
:param parents: If parents is true, any missing parents of this path
|
|
910
|
+
are created as needed; If parents is false (the default),
|
|
911
|
+
a missing parent raises FileNotFoundError.
|
|
851
912
|
:param exist_ok: If False and target directory exists, raise FileExistsError
|
|
852
913
|
|
|
853
914
|
:raises: FileExistsError
|
|
854
|
-
|
|
915
|
+
"""
|
|
855
916
|
if self.exists():
|
|
856
917
|
if not exist_ok:
|
|
857
|
-
raise FileExistsError(
|
|
858
|
-
f"File exists: '{self.path_with_protocol}'")
|
|
918
|
+
raise FileExistsError(f"File exists: '{self.path_with_protocol}'")
|
|
859
919
|
return
|
|
860
920
|
|
|
861
921
|
if parents:
|
|
@@ -866,8 +926,7 @@ class SftpPath(URIPath):
|
|
|
866
926
|
else:
|
|
867
927
|
parent_path_objects.append(parent_path_object)
|
|
868
928
|
for parent_path_object in parent_path_objects[::-1]:
|
|
869
|
-
parent_path_object.mkdir(
|
|
870
|
-
mode=mode, parents=False, exist_ok=True)
|
|
929
|
+
parent_path_object.mkdir(mode=mode, parents=False, exist_ok=True)
|
|
871
930
|
try:
|
|
872
931
|
self._client.mkdir(path=self._real_path, mode=mode)
|
|
873
932
|
except OSError:
|
|
@@ -876,29 +935,34 @@ class SftpPath(URIPath):
|
|
|
876
935
|
raise
|
|
877
936
|
|
|
878
937
|
def realpath(self) -> str:
|
|
879
|
-
|
|
938
|
+
"""Return the real path of given path
|
|
880
939
|
|
|
881
940
|
:returns: Real path of given path
|
|
882
|
-
|
|
941
|
+
"""
|
|
883
942
|
return self.resolve().path_with_protocol
|
|
884
943
|
|
|
885
|
-
def _is_same_backend(self, other:
|
|
886
|
-
return
|
|
944
|
+
def _is_same_backend(self, other: "SftpPath") -> bool:
|
|
945
|
+
return (
|
|
946
|
+
self._urlsplit_parts.hostname == other._urlsplit_parts.hostname
|
|
947
|
+
and self._urlsplit_parts.username == other._urlsplit_parts.username
|
|
948
|
+
and self._urlsplit_parts.password == other._urlsplit_parts.password
|
|
949
|
+
and self._urlsplit_parts.port == other._urlsplit_parts.port
|
|
950
|
+
)
|
|
887
951
|
|
|
888
952
|
def _is_same_protocol(self, path):
|
|
889
953
|
return is_sftp(path)
|
|
890
954
|
|
|
891
|
-
def rename(self, dst_path: PathLike, overwrite: bool = True) ->
|
|
892
|
-
|
|
955
|
+
def rename(self, dst_path: PathLike, overwrite: bool = True) -> "SftpPath":
|
|
956
|
+
"""
|
|
893
957
|
rename file on sftp
|
|
894
958
|
|
|
895
959
|
:param dst_path: Given destination path
|
|
896
960
|
:param overwrite: whether or not overwrite file when exists
|
|
897
|
-
|
|
961
|
+
"""
|
|
898
962
|
if not self._is_same_protocol(dst_path):
|
|
899
|
-
raise OSError(
|
|
963
|
+
raise OSError("Not a %s path: %r" % (self.protocol, dst_path))
|
|
900
964
|
|
|
901
|
-
dst_path = self.from_path(str(dst_path).rstrip(
|
|
965
|
+
dst_path = self.from_path(str(dst_path).rstrip("/"))
|
|
902
966
|
|
|
903
967
|
src_stat = self.stat()
|
|
904
968
|
|
|
@@ -913,12 +977,13 @@ class SftpPath(URIPath):
|
|
|
913
977
|
if self.is_dir():
|
|
914
978
|
for file_entry in self.scandir():
|
|
915
979
|
self.from_path(file_entry.path).rename(
|
|
916
|
-
dst_path.joinpath(file_entry.name)
|
|
980
|
+
dst_path.joinpath(file_entry.name)
|
|
981
|
+
)
|
|
917
982
|
self._client.rmdir(self._real_path)
|
|
918
983
|
else:
|
|
919
984
|
if overwrite or not dst_path.exists():
|
|
920
|
-
with self.open(
|
|
921
|
-
with dst_path.open(
|
|
985
|
+
with self.open("rb") as fsrc:
|
|
986
|
+
with dst_path.open("wb") as fdst:
|
|
922
987
|
length = 16 * 1024
|
|
923
988
|
while True:
|
|
924
989
|
buf = fsrc.read(length)
|
|
@@ -931,21 +996,22 @@ class SftpPath(URIPath):
|
|
|
931
996
|
dst_path.chmod(src_stat.st_mode)
|
|
932
997
|
return dst_path
|
|
933
998
|
|
|
934
|
-
def replace(self, dst_path: PathLike, overwrite: bool = True) ->
|
|
935
|
-
|
|
999
|
+
def replace(self, dst_path: PathLike, overwrite: bool = True) -> "SftpPath":
|
|
1000
|
+
"""
|
|
936
1001
|
move file on sftp
|
|
937
1002
|
|
|
938
1003
|
:param dst_path: Given destination path
|
|
939
1004
|
:param overwrite: whether or not overwrite file when exists
|
|
940
|
-
|
|
1005
|
+
"""
|
|
941
1006
|
return self.rename(dst_path=dst_path, overwrite=overwrite)
|
|
942
1007
|
|
|
943
1008
|
def remove(self, missing_ok: bool = False) -> None:
|
|
944
|
-
|
|
1009
|
+
"""
|
|
945
1010
|
Remove the file or directory on sftp
|
|
946
1011
|
|
|
947
|
-
:param missing_ok: if False and target file/directory not exists,
|
|
948
|
-
|
|
1012
|
+
:param missing_ok: if False and target file/directory not exists,
|
|
1013
|
+
raise FileNotFoundError
|
|
1014
|
+
"""
|
|
949
1015
|
if missing_ok and not self.exists():
|
|
950
1016
|
return
|
|
951
1017
|
if self.is_dir():
|
|
@@ -955,10 +1021,8 @@ class SftpPath(URIPath):
|
|
|
955
1021
|
else:
|
|
956
1022
|
self._client.unlink(self._real_path)
|
|
957
1023
|
|
|
958
|
-
def scan(self,
|
|
959
|
-
|
|
960
|
-
followlinks: bool = False) -> Iterator[str]:
|
|
961
|
-
'''
|
|
1024
|
+
def scan(self, missing_ok: bool = True, followlinks: bool = False) -> Iterator[str]:
|
|
1025
|
+
"""
|
|
962
1026
|
Iteratively traverse only files in given directory, in alphabetical order.
|
|
963
1027
|
Every iteration on generator yields a path string.
|
|
964
1028
|
|
|
@@ -966,84 +1030,89 @@ class SftpPath(URIPath):
|
|
|
966
1030
|
If path is a non-existent path, return an empty generator
|
|
967
1031
|
If path is a bucket path, return all file paths in the bucket
|
|
968
1032
|
|
|
969
|
-
:param missing_ok: If False and there's no file in the directory,
|
|
1033
|
+
:param missing_ok: If False and there's no file in the directory,
|
|
1034
|
+
raise FileNotFoundError
|
|
970
1035
|
:returns: A file path generator
|
|
971
|
-
|
|
972
|
-
scan_stat_iter = self.scan_stat(
|
|
973
|
-
missing_ok=missing_ok, followlinks=followlinks)
|
|
1036
|
+
"""
|
|
1037
|
+
scan_stat_iter = self.scan_stat(missing_ok=missing_ok, followlinks=followlinks)
|
|
974
1038
|
|
|
975
1039
|
for file_entry in scan_stat_iter:
|
|
976
1040
|
yield file_entry.path
|
|
977
1041
|
|
|
978
|
-
def scan_stat(
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1042
|
+
def scan_stat(
|
|
1043
|
+
self, missing_ok: bool = True, followlinks: bool = False
|
|
1044
|
+
) -> Iterator[FileEntry]:
|
|
1045
|
+
"""
|
|
982
1046
|
Iteratively traverse only files in given directory, in alphabetical order.
|
|
983
1047
|
Every iteration on generator yields a tuple of path string and file stat
|
|
984
1048
|
|
|
985
|
-
:param missing_ok: If False and there's no file in the directory,
|
|
1049
|
+
:param missing_ok: If False and there's no file in the directory,
|
|
1050
|
+
raise FileNotFoundError
|
|
986
1051
|
:returns: A file path generator
|
|
987
|
-
|
|
1052
|
+
"""
|
|
988
1053
|
|
|
989
1054
|
def create_generator() -> Iterator[FileEntry]:
|
|
990
|
-
|
|
991
1055
|
try:
|
|
992
1056
|
stat = self.stat(follow_symlinks=followlinks)
|
|
993
1057
|
except FileNotFoundError:
|
|
994
1058
|
return
|
|
995
1059
|
if S_ISREG(stat.st_mode):
|
|
996
1060
|
yield FileEntry(
|
|
997
|
-
self.name,
|
|
998
|
-
self.
|
|
1061
|
+
self.name,
|
|
1062
|
+
self.path_with_protocol,
|
|
1063
|
+
self.stat(follow_symlinks=followlinks),
|
|
1064
|
+
)
|
|
999
1065
|
return
|
|
1000
1066
|
|
|
1001
1067
|
for name in self.listdir():
|
|
1002
1068
|
current_path = self.joinpath(name)
|
|
1003
1069
|
if current_path.is_dir():
|
|
1004
1070
|
yield from current_path.scan_stat(
|
|
1005
|
-
missing_ok=missing_ok,
|
|
1006
|
-
followlinks=followlinks,
|
|
1071
|
+
missing_ok=missing_ok, followlinks=followlinks
|
|
1007
1072
|
)
|
|
1008
1073
|
else:
|
|
1009
1074
|
yield FileEntry(
|
|
1010
|
-
current_path.name,
|
|
1011
|
-
current_path.
|
|
1075
|
+
current_path.name,
|
|
1076
|
+
current_path.path_with_protocol,
|
|
1077
|
+
current_path.stat(follow_symlinks=followlinks),
|
|
1078
|
+
)
|
|
1012
1079
|
|
|
1013
1080
|
return _create_missing_ok_generator(
|
|
1014
|
-
create_generator(),
|
|
1015
|
-
|
|
1016
|
-
|
|
1081
|
+
create_generator(),
|
|
1082
|
+
missing_ok,
|
|
1083
|
+
FileNotFoundError("No match any file in: %r" % self.path_with_protocol),
|
|
1084
|
+
)
|
|
1017
1085
|
|
|
1018
1086
|
def scandir(self) -> Iterator[FileEntry]:
|
|
1019
|
-
|
|
1087
|
+
"""
|
|
1020
1088
|
Get all content of given file path.
|
|
1021
1089
|
|
|
1022
1090
|
:returns: An iterator contains all contents have prefix path
|
|
1023
|
-
|
|
1091
|
+
"""
|
|
1024
1092
|
if not self.exists():
|
|
1025
|
-
raise FileNotFoundError(
|
|
1026
|
-
'No such directory: %r' % self.path_with_protocol)
|
|
1093
|
+
raise FileNotFoundError("No such directory: %r" % self.path_with_protocol)
|
|
1027
1094
|
|
|
1028
1095
|
if not self.is_dir():
|
|
1029
|
-
raise NotADirectoryError(
|
|
1030
|
-
'Not a directory: %r' % self.path_with_protocol)
|
|
1096
|
+
raise NotADirectoryError("Not a directory: %r" % self.path_with_protocol)
|
|
1031
1097
|
|
|
1032
1098
|
def create_generator():
|
|
1033
1099
|
for name in self.listdir():
|
|
1034
1100
|
current_path = self.joinpath(name)
|
|
1035
1101
|
yield FileEntry(
|
|
1036
|
-
current_path.name,
|
|
1037
|
-
current_path.
|
|
1102
|
+
current_path.name,
|
|
1103
|
+
current_path.path_with_protocol,
|
|
1104
|
+
current_path.lstat(),
|
|
1105
|
+
)
|
|
1038
1106
|
|
|
1039
1107
|
return ContextIterator(create_generator())
|
|
1040
1108
|
|
|
1041
1109
|
def stat(self, follow_symlinks=True) -> StatResult:
|
|
1042
|
-
|
|
1043
|
-
Get StatResult of file on sftp, including file size and mtime,
|
|
1110
|
+
"""
|
|
1111
|
+
Get StatResult of file on sftp, including file size and mtime,
|
|
1112
|
+
referring to fs_getsize and fs_getmtime
|
|
1044
1113
|
|
|
1045
1114
|
:returns: StatResult
|
|
1046
|
-
|
|
1115
|
+
"""
|
|
1047
1116
|
if follow_symlinks:
|
|
1048
1117
|
result = _make_stat(self._client.stat(self._real_path))
|
|
1049
1118
|
else:
|
|
@@ -1051,37 +1120,41 @@ class SftpPath(URIPath):
|
|
|
1051
1120
|
return result
|
|
1052
1121
|
|
|
1053
1122
|
def unlink(self, missing_ok: bool = False) -> None:
|
|
1054
|
-
|
|
1123
|
+
"""
|
|
1055
1124
|
Remove the file on sftp
|
|
1056
1125
|
|
|
1057
1126
|
:param missing_ok: if False and target file not exists, raise FileNotFoundError
|
|
1058
|
-
|
|
1127
|
+
"""
|
|
1059
1128
|
if missing_ok and not self.exists():
|
|
1060
1129
|
return
|
|
1061
1130
|
self._client.unlink(self._real_path)
|
|
1062
1131
|
|
|
1063
1132
|
def walk(
|
|
1064
|
-
self,
|
|
1065
|
-
followlinks: bool = False
|
|
1133
|
+
self, followlinks: bool = False
|
|
1066
1134
|
) -> Iterator[Tuple[str, List[str], List[str]]]:
|
|
1067
|
-
|
|
1135
|
+
"""
|
|
1068
1136
|
Generate the file names in a directory tree by walking the tree top-down.
|
|
1069
1137
|
For each directory in the tree rooted at directory path (including path itself),
|
|
1070
1138
|
it yields a 3-tuple (root, dirs, files).
|
|
1071
1139
|
|
|
1072
|
-
root: a string of current path
|
|
1073
|
-
dirs: name list of subdirectories (excluding '.' and '..' if they exist)
|
|
1074
|
-
|
|
1140
|
+
- root: a string of current path
|
|
1141
|
+
- dirs: name list of subdirectories (excluding '.' and '..' if they exist)
|
|
1142
|
+
in 'root'. The list is sorted by ascending alphabetical order
|
|
1143
|
+
- files: name list of non-directory files (link is regarded as file) in 'root'.
|
|
1144
|
+
The list is sorted by ascending alphabetical order
|
|
1075
1145
|
|
|
1076
|
-
If path not exists, or path is a file (link is regarded as file),
|
|
1146
|
+
If path not exists, or path is a file (link is regarded as file),
|
|
1147
|
+
return an empty generator
|
|
1077
1148
|
|
|
1078
1149
|
.. note::
|
|
1079
1150
|
|
|
1080
|
-
Be aware that setting ``followlinks`` to True can lead to infinite recursion
|
|
1151
|
+
Be aware that setting ``followlinks`` to True can lead to infinite recursion
|
|
1152
|
+
if a link points to a parent directory of itself. fs_walk() does not keep
|
|
1153
|
+
track of the directories it visited already.
|
|
1081
1154
|
|
|
1082
1155
|
:param followlinks: False if regard symlink as file, else True
|
|
1083
1156
|
:returns: A 3-tuple generator
|
|
1084
|
-
|
|
1157
|
+
"""
|
|
1085
1158
|
if not self.exists(followlinks=followlinks):
|
|
1086
1159
|
return
|
|
1087
1160
|
|
|
@@ -1103,140 +1176,148 @@ class SftpPath(URIPath):
|
|
|
1103
1176
|
dirs = sorted(dirs)
|
|
1104
1177
|
files = sorted(files)
|
|
1105
1178
|
|
|
1106
|
-
yield self._generate_path_object(
|
|
1107
|
-
root).path_with_protocol, dirs, files
|
|
1179
|
+
yield self._generate_path_object(root).path_with_protocol, dirs, files
|
|
1108
1180
|
|
|
1109
1181
|
stack.extend(
|
|
1110
|
-
(os.path.join(root, directory) for directory in reversed(dirs))
|
|
1182
|
+
(os.path.join(root, directory) for directory in reversed(dirs))
|
|
1183
|
+
)
|
|
1111
1184
|
|
|
1112
|
-
def resolve(self, strict=False) ->
|
|
1113
|
-
|
|
1185
|
+
def resolve(self, strict=False) -> "SftpPath":
|
|
1186
|
+
"""Equal to sftp_realpath
|
|
1114
1187
|
|
|
1115
1188
|
:param strict: Ignore this parameter, just for compatibility
|
|
1116
|
-
:return: Return the canonical path of the specified filename,
|
|
1189
|
+
:return: Return the canonical path of the specified filename,
|
|
1190
|
+
eliminating any symbolic links encountered in the path.
|
|
1117
1191
|
:rtype: SftpPath
|
|
1118
|
-
|
|
1192
|
+
"""
|
|
1119
1193
|
path = self._client.normalize(self._real_path)
|
|
1120
1194
|
return self._generate_path_object(path, resolve=True)
|
|
1121
1195
|
|
|
1122
1196
|
def md5(self, recalculate: bool = False, followlinks: bool = True):
|
|
1123
|
-
|
|
1197
|
+
"""
|
|
1124
1198
|
Calculate the md5 value of the file
|
|
1125
1199
|
|
|
1126
1200
|
:param recalculate: Ignore this parameter, just for compatibility
|
|
1127
1201
|
:param followlinks: Ignore this parameter, just for compatibility
|
|
1128
1202
|
|
|
1129
1203
|
returns: md5 of file
|
|
1130
|
-
|
|
1204
|
+
"""
|
|
1131
1205
|
if self.is_dir():
|
|
1132
1206
|
hash_md5 = hashlib.md5() # nosec
|
|
1133
1207
|
for file_name in self.listdir():
|
|
1134
|
-
chunk =
|
|
1135
|
-
|
|
1208
|
+
chunk = (
|
|
1209
|
+
self.joinpath(file_name)
|
|
1210
|
+
.md5(recalculate=recalculate, followlinks=followlinks)
|
|
1211
|
+
.encode()
|
|
1212
|
+
)
|
|
1136
1213
|
hash_md5.update(chunk)
|
|
1137
1214
|
return hash_md5.hexdigest()
|
|
1138
|
-
with self.open(
|
|
1215
|
+
with self.open("rb") as src:
|
|
1139
1216
|
md5 = calculate_md5(src)
|
|
1140
1217
|
return md5
|
|
1141
1218
|
|
|
1142
1219
|
def symlink(self, dst_path: PathLike) -> None:
|
|
1143
|
-
|
|
1220
|
+
"""
|
|
1144
1221
|
Create a symbolic link pointing to src_path named dst_path.
|
|
1145
1222
|
|
|
1146
1223
|
:param dst_path: Destination path
|
|
1147
|
-
|
|
1224
|
+
"""
|
|
1148
1225
|
dst_path = self.from_path(dst_path)
|
|
1149
1226
|
if dst_path.exists(followlinks=False):
|
|
1150
|
-
raise FileExistsError(
|
|
1151
|
-
f"File exists: '{dst_path.path_with_protocol}'")
|
|
1227
|
+
raise FileExistsError(f"File exists: '{dst_path.path_with_protocol}'")
|
|
1152
1228
|
return self._client.symlink(self._real_path, dst_path._real_path)
|
|
1153
1229
|
|
|
1154
|
-
def readlink(self) ->
|
|
1155
|
-
|
|
1156
|
-
Return a SftpPath instance representing the path to
|
|
1157
|
-
|
|
1158
|
-
|
|
1230
|
+
def readlink(self) -> "SftpPath":
|
|
1231
|
+
"""
|
|
1232
|
+
Return a SftpPath instance representing the path to
|
|
1233
|
+
which the symbolic link points.
|
|
1234
|
+
"""
|
|
1159
1235
|
if not self.is_symlink():
|
|
1160
|
-
raise OSError(
|
|
1236
|
+
raise OSError("Not a symlink: %s" % self.path_with_protocol)
|
|
1161
1237
|
path = self._client.readlink(self._real_path)
|
|
1162
1238
|
if not path:
|
|
1163
|
-
raise OSError(
|
|
1164
|
-
if not path.startswith(
|
|
1239
|
+
raise OSError("Not a symlink: %s" % self.path_with_protocol)
|
|
1240
|
+
if not path.startswith("/"):
|
|
1165
1241
|
return self.parent.joinpath(path)
|
|
1166
1242
|
return self._generate_path_object(path)
|
|
1167
1243
|
|
|
1168
1244
|
def is_symlink(self) -> bool:
|
|
1169
|
-
|
|
1245
|
+
"""Test whether a path is a symbolic link
|
|
1170
1246
|
|
|
1171
1247
|
:return: If path is a symbolic link return True, else False
|
|
1172
1248
|
:rtype: bool
|
|
1173
|
-
|
|
1249
|
+
"""
|
|
1174
1250
|
return self.lstat().is_symlink()
|
|
1175
1251
|
|
|
1176
|
-
def cwd(self) ->
|
|
1177
|
-
|
|
1252
|
+
def cwd(self) -> "SftpPath":
|
|
1253
|
+
"""Return current working directory
|
|
1178
1254
|
|
|
1179
1255
|
returns: Current working directory
|
|
1180
|
-
|
|
1181
|
-
return self._generate_path_object(self._client.normalize(
|
|
1256
|
+
"""
|
|
1257
|
+
return self._generate_path_object(self._client.normalize("."))
|
|
1182
1258
|
|
|
1183
1259
|
def save(self, file_object: BinaryIO):
|
|
1184
|
-
|
|
1260
|
+
"""Write the opened binary stream to path
|
|
1185
1261
|
If parent directory of path doesn't exist, it will be created.
|
|
1186
1262
|
|
|
1187
1263
|
:param file_object: stream to be read
|
|
1188
|
-
|
|
1189
|
-
with self.open(mode=
|
|
1264
|
+
"""
|
|
1265
|
+
with self.open(mode="wb") as output:
|
|
1190
1266
|
output.write(file_object.read())
|
|
1191
1267
|
|
|
1192
1268
|
def open(
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1269
|
+
self,
|
|
1270
|
+
mode: str = "r",
|
|
1271
|
+
buffering=-1,
|
|
1272
|
+
encoding: Optional[str] = None,
|
|
1273
|
+
errors: Optional[str] = None,
|
|
1274
|
+
**kwargs,
|
|
1275
|
+
) -> IO:
|
|
1276
|
+
"""Open a file on the path.
|
|
1200
1277
|
|
|
1201
1278
|
:param mode: Mode to open file
|
|
1202
|
-
:param buffering: buffering is an optional integer used to
|
|
1203
|
-
|
|
1204
|
-
:param
|
|
1279
|
+
:param buffering: buffering is an optional integer used to
|
|
1280
|
+
set the buffering policy.
|
|
1281
|
+
:param encoding: encoding is the name of the encoding used to decode or encode
|
|
1282
|
+
the file. This should only be used in text mode.
|
|
1283
|
+
:param errors: errors is an optional string that specifies how encoding and
|
|
1284
|
+
decoding errors are to be handled—this cannot be used in binary mode.
|
|
1205
1285
|
:returns: File-Like object
|
|
1206
|
-
|
|
1207
|
-
if
|
|
1286
|
+
"""
|
|
1287
|
+
if "w" in mode or "x" in mode or "a" in mode:
|
|
1208
1288
|
if self.is_dir():
|
|
1209
|
-
raise IsADirectoryError(
|
|
1210
|
-
'Is a directory: %r' % self.path_with_protocol)
|
|
1289
|
+
raise IsADirectoryError("Is a directory: %r" % self.path_with_protocol)
|
|
1211
1290
|
self.parent.mkdir(parents=True, exist_ok=True)
|
|
1212
1291
|
elif not self.exists():
|
|
1213
|
-
raise FileNotFoundError(
|
|
1214
|
-
'No such file: %r' % self.path_with_protocol)
|
|
1292
|
+
raise FileNotFoundError("No such file: %r" % self.path_with_protocol)
|
|
1215
1293
|
fileobj = self._client.open(self._real_path, mode, bufsize=buffering)
|
|
1216
1294
|
fileobj.name = self.path
|
|
1217
|
-
if
|
|
1218
|
-
return io.TextIOWrapper(
|
|
1295
|
+
if "r" in mode and "b" not in mode:
|
|
1296
|
+
return io.TextIOWrapper(
|
|
1297
|
+
fileobj, encoding=encoding, errors=errors
|
|
1298
|
+
) # pytype: disable=wrong-arg-types
|
|
1219
1299
|
return fileobj # pytype: disable=bad-return-type
|
|
1220
1300
|
|
|
1221
1301
|
def chmod(self, mode: int, follow_symlinks: bool = True):
|
|
1222
|
-
|
|
1302
|
+
"""
|
|
1223
1303
|
Change the file mode and permissions, like os.chmod().
|
|
1224
1304
|
|
|
1225
1305
|
:param mode: the file mode you want to change
|
|
1226
1306
|
:param followlinks: Ignore this parameter, just for compatibility
|
|
1227
|
-
|
|
1307
|
+
"""
|
|
1228
1308
|
return self._client.chmod(path=self._real_path, mode=mode)
|
|
1229
1309
|
|
|
1230
|
-
def absolute(self) ->
|
|
1231
|
-
|
|
1232
|
-
Make the path absolute, without normalization or resolving symlinks.
|
|
1233
|
-
|
|
1310
|
+
def absolute(self) -> "SftpPath":
|
|
1311
|
+
"""
|
|
1312
|
+
Make the path absolute, without normalization or resolving symlinks.
|
|
1313
|
+
Returns a new path object
|
|
1314
|
+
"""
|
|
1234
1315
|
return self.resolve()
|
|
1235
1316
|
|
|
1236
1317
|
def rmdir(self):
|
|
1237
|
-
|
|
1318
|
+
"""
|
|
1238
1319
|
Remove this directory. The directory must be empty.
|
|
1239
|
-
|
|
1320
|
+
"""
|
|
1240
1321
|
if len(self.listdir()) > 0:
|
|
1241
1322
|
raise OSError(f"Directory not empty: '{self.path_with_protocol}'")
|
|
1242
1323
|
return self._client.rmdir(self._real_path)
|
|
@@ -1249,35 +1330,42 @@ class SftpPath(URIPath):
|
|
|
1249
1330
|
environment: Optional[dict] = None,
|
|
1250
1331
|
) -> subprocess.CompletedProcess:
|
|
1251
1332
|
with get_ssh_session(
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1333
|
+
hostname=self._urlsplit_parts.hostname,
|
|
1334
|
+
port=self._urlsplit_parts.port,
|
|
1335
|
+
username=self._urlsplit_parts.username,
|
|
1336
|
+
password=self._urlsplit_parts.password,
|
|
1256
1337
|
) as chan:
|
|
1257
1338
|
chan.settimeout(timeout)
|
|
1258
1339
|
if environment:
|
|
1259
1340
|
chan.update_environment(environment)
|
|
1260
|
-
chan.exec_command(
|
|
1261
|
-
stdout =
|
|
1262
|
-
"r", bufsize).read().decode(errors="backslashreplace")
|
|
1263
|
-
|
|
1264
|
-
|
|
1341
|
+
chan.exec_command(" ".join([shlex.quote(arg) for arg in command]))
|
|
1342
|
+
stdout = (
|
|
1343
|
+
chan.makefile("r", bufsize).read().decode(errors="backslashreplace")
|
|
1344
|
+
)
|
|
1345
|
+
stderr = (
|
|
1346
|
+
chan.makefile_stderr("r", bufsize)
|
|
1347
|
+
.read()
|
|
1348
|
+
.decode(errors="backslashreplace")
|
|
1349
|
+
)
|
|
1265
1350
|
returncode = chan.recv_exit_status()
|
|
1266
1351
|
return subprocess.CompletedProcess(
|
|
1267
|
-
args=command, returncode=returncode, stdout=stdout, stderr=stderr
|
|
1352
|
+
args=command, returncode=returncode, stdout=stdout, stderr=stderr
|
|
1353
|
+
)
|
|
1268
1354
|
|
|
1269
1355
|
def copy(
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1356
|
+
self,
|
|
1357
|
+
dst_path: PathLike,
|
|
1358
|
+
callback: Optional[Callable[[int], None]] = None,
|
|
1359
|
+
followlinks: bool = False,
|
|
1360
|
+
overwrite: bool = True,
|
|
1361
|
+
):
|
|
1275
1362
|
"""
|
|
1276
1363
|
Copy the file to the given destination path.
|
|
1277
1364
|
|
|
1278
1365
|
:param dst_path: The destination path to copy the file to.
|
|
1279
|
-
:param callback: An optional callback function that takes an integer parameter
|
|
1280
|
-
|
|
1366
|
+
:param callback: An optional callback function that takes an integer parameter
|
|
1367
|
+
and is called periodically during the copy operation to report the number
|
|
1368
|
+
of bytes copied.
|
|
1281
1369
|
:param followlinks: Whether to follow symbolic links when copying directories.
|
|
1282
1370
|
:raises IsADirectoryError: If the source is a directory.
|
|
1283
1371
|
:raises OSError: If there is an error copying the file.
|
|
@@ -1286,35 +1374,34 @@ class SftpPath(URIPath):
|
|
|
1286
1374
|
return self.readlink().copy(dst_path=dst_path, callback=callback)
|
|
1287
1375
|
|
|
1288
1376
|
if not self._is_same_protocol(dst_path):
|
|
1289
|
-
raise OSError(
|
|
1290
|
-
if str(dst_path).endswith(
|
|
1291
|
-
raise IsADirectoryError(
|
|
1377
|
+
raise OSError("Not a %s path: %r" % (self.protocol, dst_path))
|
|
1378
|
+
if str(dst_path).endswith("/"):
|
|
1379
|
+
raise IsADirectoryError("Is a directory: %r" % dst_path)
|
|
1292
1380
|
|
|
1293
1381
|
if self.is_dir():
|
|
1294
|
-
raise IsADirectoryError(
|
|
1295
|
-
'Is a directory: %r' % self.path_with_protocol)
|
|
1382
|
+
raise IsADirectoryError("Is a directory: %r" % self.path_with_protocol)
|
|
1296
1383
|
|
|
1297
1384
|
if not overwrite and self.from_path(dst_path).exists():
|
|
1298
1385
|
return
|
|
1299
1386
|
|
|
1300
|
-
self.from_path(os.path.dirname(
|
|
1301
|
-
fspath(dst_path))).makedirs(exist_ok=True)
|
|
1387
|
+
self.from_path(os.path.dirname(fspath(dst_path))).makedirs(exist_ok=True)
|
|
1302
1388
|
dst_path = self.from_path(dst_path)
|
|
1303
1389
|
if self._is_same_backend(dst_path):
|
|
1304
1390
|
if self._real_path == dst_path._real_path:
|
|
1305
1391
|
raise SameFileError(
|
|
1306
|
-
f"'{self.path}' and '{dst_path.path}' are the same file"
|
|
1392
|
+
f"'{self.path}' and '{dst_path.path}' are the same file"
|
|
1393
|
+
)
|
|
1307
1394
|
exec_result = self._exec_command(
|
|
1308
|
-
["cp", self._real_path, dst_path._real_path]
|
|
1395
|
+
["cp", self._real_path, dst_path._real_path]
|
|
1396
|
+
)
|
|
1309
1397
|
if exec_result.returncode != 0:
|
|
1310
1398
|
_logger.error(exec_result.stderr)
|
|
1311
|
-
raise OSError(
|
|
1312
|
-
f'Copy file error, returncode: {exec_result.returncode}')
|
|
1399
|
+
raise OSError(f"Copy file error, returncode: {exec_result.returncode}")
|
|
1313
1400
|
if callback:
|
|
1314
1401
|
callback(self.stat(follow_symlinks=followlinks).size)
|
|
1315
1402
|
else:
|
|
1316
|
-
with self.open(
|
|
1317
|
-
with dst_path.open(
|
|
1403
|
+
with self.open("rb") as fsrc:
|
|
1404
|
+
with dst_path.open("wb") as fdst:
|
|
1318
1405
|
length = 16 * 1024
|
|
1319
1406
|
while True:
|
|
1320
1407
|
buf = fsrc.read(length)
|
|
@@ -1329,23 +1416,26 @@ class SftpPath(URIPath):
|
|
|
1329
1416
|
dst_path._client.chmod(dst_path._real_path, src_stat.st_mode)
|
|
1330
1417
|
|
|
1331
1418
|
def sync(
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1419
|
+
self,
|
|
1420
|
+
dst_path: PathLike,
|
|
1421
|
+
followlinks: bool = False,
|
|
1422
|
+
force: bool = False,
|
|
1423
|
+
overwrite: bool = True,
|
|
1424
|
+
):
|
|
1425
|
+
"""Copy file/directory on src_url to dst_url
|
|
1338
1426
|
|
|
1339
1427
|
:param dst_url: Given destination path
|
|
1340
1428
|
:param followlinks: False if regard symlink as file, else True
|
|
1341
|
-
:param force: Sync file forcible, do not ignore same files,
|
|
1429
|
+
:param force: Sync file forcible, do not ignore same files,
|
|
1430
|
+
priority is higher than 'overwrite', default is False
|
|
1342
1431
|
:param overwrite: whether or not overwrite file when exists, default is True
|
|
1343
|
-
|
|
1432
|
+
"""
|
|
1344
1433
|
if not self._is_same_protocol(dst_path):
|
|
1345
|
-
raise OSError(
|
|
1434
|
+
raise OSError("Not a %s path: %r" % (self.protocol, dst_path))
|
|
1346
1435
|
|
|
1347
1436
|
for src_file_path, dst_file_path in _sftp_scan_pairs(
|
|
1348
|
-
|
|
1437
|
+
self.path_with_protocol, dst_path
|
|
1438
|
+
):
|
|
1349
1439
|
dst_path = self.from_path(dst_file_path)
|
|
1350
1440
|
src_path = self.from_path(src_file_path)
|
|
1351
1441
|
|
|
@@ -1353,12 +1443,12 @@ class SftpPath(URIPath):
|
|
|
1353
1443
|
pass
|
|
1354
1444
|
elif not overwrite and dst_path.exists():
|
|
1355
1445
|
continue
|
|
1356
|
-
elif dst_path.exists() and is_same_file(
|
|
1357
|
-
|
|
1446
|
+
elif dst_path.exists() and is_same_file(
|
|
1447
|
+
src_path.stat(), dst_path.stat(), "copy"
|
|
1448
|
+
):
|
|
1358
1449
|
continue
|
|
1359
1450
|
|
|
1360
|
-
self.from_path(src_file_path).copy(
|
|
1361
|
-
dst_file_path, followlinks=followlinks)
|
|
1451
|
+
self.from_path(src_file_path).copy(dst_file_path, followlinks=followlinks)
|
|
1362
1452
|
|
|
1363
1453
|
def utime(self, atime: Union[float, int], mtime: Union[float, int]) -> None:
|
|
1364
1454
|
"""
|