megfile 3.0.4__py3-none-any.whl → 3.0.6__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.
megfile/s3_path.py CHANGED
@@ -12,7 +12,7 @@ import boto3
12
12
  import botocore
13
13
  from botocore.awsrequest import AWSResponse
14
14
 
15
- from megfile.config import DEFAULT_BLOCK_SIZE, GLOBAL_MAX_WORKERS, S3_CLIENT_CACHE_MODE
15
+ from megfile.config import DEFAULT_BLOCK_SIZE, GLOBAL_MAX_WORKERS, S3_CLIENT_CACHE_MODE, S3_MAX_RETRY_TIMES
16
16
  from megfile.errors import S3BucketNotFoundError, S3ConfigError, S3FileExistsError, S3FileNotFoundError, S3IsADirectoryError, S3NameTooLongError, S3NotADirectoryError, S3NotALinkError, S3PermissionError, S3UnknownError, SameFileError, UnsupportedError, _create_missing_ok_generator
17
17
  from megfile.errors import _logger as error_logger
18
18
  from megfile.errors import patch_method, raise_s3_error, s3_error_code_should_retry, s3_should_retry, translate_fs_error, translate_s3_error
@@ -69,7 +69,7 @@ _logger = get_logger(__name__)
69
69
  content_md5_header = 'megfile-content-md5'
70
70
  endpoint_url = 'https://s3.amazonaws.com'
71
71
  max_pool_connections = GLOBAL_MAX_WORKERS # for compatibility
72
- max_retries = 10
72
+ max_retries = S3_MAX_RETRY_TIMES
73
73
  max_keys = 1000
74
74
 
75
75
 
@@ -930,16 +930,29 @@ def s3_download(
930
930
  src_url: PathLike,
931
931
  dst_url: PathLike,
932
932
  followlinks: bool = False,
933
- callback: Optional[Callable[[int], None]] = None) -> None:
933
+ callback: Optional[Callable[[int], None]] = None,
934
+ overwrite: bool = True) -> None:
934
935
  '''
935
936
  Downloads a file from s3 to local filesystem.
936
937
  :param src_url: source s3 path
937
938
  :param dst_url: target fs path
938
939
  :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
940
+ :param followlinks: False if regard symlink as file, else True
941
+ :param overwrite: whether or not overwrite file when exists, default is True
939
942
  '''
940
943
  from megfile.fs import is_fs
941
944
  from megfile.fs_path import FSPath
942
945
 
946
+ dst_url = fspath(dst_url)
947
+ if not is_fs(dst_url):
948
+ raise OSError(f'dst_url is not fs path: {dst_url}')
949
+ if not dst_url or dst_url.endswith('/'):
950
+ raise S3IsADirectoryError('Is a directory: %r' % dst_url)
951
+
952
+ dst_path = FSPath(dst_url)
953
+ if not overwrite and dst_path.exists():
954
+ return
955
+
943
956
  if not isinstance(src_url, S3Path):
944
957
  src_url = S3Path(src_url)
945
958
  if followlinks:
@@ -960,13 +973,6 @@ def s3_download(
960
973
  raise S3IsADirectoryError(
961
974
  'Is a directory: %r' % src_url.path_with_protocol)
962
975
 
963
- dst_url = fspath(dst_url)
964
- if not is_fs(dst_url):
965
- raise OSError(f'dst_url is not fs path: {dst_url}')
966
- if not dst_url or dst_url.endswith('/'):
967
- raise S3IsADirectoryError('Is a directory: %r' % dst_url)
968
-
969
- dst_path = FSPath(dst_url)
970
976
  dst_directory = os.path.dirname(dst_path.path_without_protocol)
971
977
  if dst_directory != '':
972
978
  os.makedirs(dst_directory, exist_ok=True)
@@ -997,12 +1003,14 @@ def s3_upload(
997
1003
  src_url: PathLike,
998
1004
  dst_url: PathLike,
999
1005
  callback: Optional[Callable[[int], None]] = None,
1006
+ overwrite: bool = True,
1000
1007
  **kwargs) -> None:
1001
1008
  '''
1002
1009
  Uploads a file from local filesystem to s3.
1003
1010
  :param src_url: source fs path
1004
1011
  :param dst_url: target s3 path
1005
1012
  :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
1013
+ :param overwrite: whether or not overwrite file when exists, default is True
1006
1014
  '''
1007
1015
  from megfile.fs import is_fs
1008
1016
  from megfile.fs_path import FSPath
@@ -1017,6 +1025,9 @@ def s3_upload(
1017
1025
  if not dst_key or dst_key.endswith('/'):
1018
1026
  raise S3IsADirectoryError('Is a directory: %r' % dst_url)
1019
1027
 
1028
+ if not overwrite and S3Path(dst_url).is_file():
1029
+ return
1030
+
1020
1031
  client = get_s3_client_with_cache(
1021
1032
  profile_name=S3Path(dst_url)._profile_name)
1022
1033
  upload_fileobj = patch_method(
@@ -1086,18 +1097,15 @@ def s3_readlink(path) -> str:
1086
1097
  return S3Path(path).readlink().path_with_protocol
1087
1098
 
1088
1099
 
1089
- def s3_rename(src_url: PathLike, dst_url: PathLike):
1100
+ def s3_rename(
1101
+ src_url: PathLike, dst_url: PathLike, overwrite: bool = True) -> None:
1090
1102
  '''
1091
1103
  Move s3 file path from src_url to dst_url
1092
1104
 
1093
1105
  :param dst_url: Given destination path
1106
+ :param overwrite: whether or not overwrite file when exists
1094
1107
  '''
1095
- src_path_ins = S3Path(src_url)
1096
- if src_path_ins.is_file():
1097
- src_path_ins.copy(dst_url)
1098
- else:
1099
- src_path_ins.sync(dst_url)
1100
- src_path_ins.remove()
1108
+ S3Path(src_url).rename(dst_url, overwrite)
1101
1109
 
1102
1110
 
1103
1111
  class S3Cacher(FileCacher):
@@ -1692,15 +1700,16 @@ class S3Path(URIPath):
1692
1700
  if self.exists():
1693
1701
  raise S3FileExistsError('File exists: %r' % self.path_with_protocol)
1694
1702
 
1695
- def move(self, dst_url: PathLike) -> None:
1703
+ def move(self, dst_url: PathLike, overwrite: bool = True) -> None:
1696
1704
  '''
1697
1705
  Move file/directory path from src_url to dst_url
1698
1706
 
1699
1707
  :param dst_url: Given destination path
1708
+ :param overwrite: whether or not overwrite file when exists
1700
1709
  '''
1701
1710
  for src_file_path, dst_file_path in _s3_scan_pairs(
1702
1711
  self.path_with_protocol, dst_url):
1703
- S3Path(src_file_path).rename(dst_file_path)
1712
+ S3Path(src_file_path).rename(dst_file_path, overwrite)
1704
1713
 
1705
1714
  def remove(self, missing_ok: bool = False) -> None:
1706
1715
  '''
@@ -1774,13 +1783,18 @@ class S3Path(URIPath):
1774
1783
  raise S3UnknownError(
1775
1784
  Exception(error_msg), self.path_with_protocol)
1776
1785
 
1777
- def rename(self, dst_path: PathLike) -> 'S3Path':
1786
+ def rename(self, dst_path: PathLike, overwrite: bool = True) -> 'S3Path':
1778
1787
  '''
1779
1788
  Move s3 file path from src_url to dst_url
1780
1789
 
1781
1790
  :param dst_path: Given destination path
1791
+ :param overwrite: whether or not overwrite file when exists
1782
1792
  '''
1783
- s3_rename(self.path_with_protocol, dst_path)
1793
+ if self.is_file():
1794
+ self.copy(dst_path, overwrite=overwrite)
1795
+ else:
1796
+ self.sync(dst_path, overwrite=overwrite)
1797
+ self.remove(missing_ok=True)
1784
1798
  return self.from_path(dst_path)
1785
1799
 
1786
1800
  def scan(self, missing_ok: bool = True,
@@ -2111,15 +2125,21 @@ class S3Path(URIPath):
2111
2125
  def copy(
2112
2126
  self,
2113
2127
  dst_url: PathLike,
2128
+ callback: Optional[Callable[[int], None]] = None,
2114
2129
  followlinks: bool = False,
2115
- callback: Optional[Callable[[int], None]] = None) -> None:
2130
+ overwrite: bool = True) -> None:
2116
2131
  ''' File copy on S3
2117
2132
  Copy content of file on `src_path` to `dst_path`.
2118
2133
  It's caller's responsebility to ensure the s3_isfile(src_url) == True
2119
2134
 
2120
2135
  :param dst_path: Target file path
2121
2136
  :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
2137
+ :param followlinks: False if regard symlink as file, else True
2138
+ :param overwrite: whether or not overwrite file when exists, default is True
2122
2139
  '''
2140
+ if not overwrite and self.from_path(dst_url).is_file():
2141
+ return
2142
+
2123
2143
  src_url = self.path_with_protocol
2124
2144
  src_bucket, src_key = parse_s3_url(src_url)
2125
2145
  dst_bucket, dst_key = parse_s3_url(dst_url)
@@ -2156,22 +2176,32 @@ class S3Path(URIPath):
2156
2176
  Callback=callback)
2157
2177
 
2158
2178
  def sync(
2159
- self, dst_url: PathLike, followlinks: bool = False,
2160
- force: bool = False) -> None:
2179
+ self,
2180
+ dst_url: PathLike,
2181
+ followlinks: bool = False,
2182
+ force: bool = False,
2183
+ overwrite: bool = True) -> None:
2161
2184
  '''
2162
2185
  Copy file/directory on src_url to dst_url
2163
2186
 
2164
2187
  :param dst_url: Given destination path
2165
2188
  :param followlinks: False if regard symlink as file, else True
2166
- :param force: Sync file forcely, do not ignore same files
2189
+ :param force: Sync file forcely, do not ignore same files, priority is higher than 'overwrite', default is False
2190
+ :param overwrite: whether or not overwrite file when exists, default is True
2167
2191
  '''
2168
2192
  for src_file_path, dst_file_path in _s3_scan_pairs(
2169
2193
  self.path_with_protocol, dst_url):
2170
2194
  src_file_path = self.from_path(src_file_path)
2171
2195
  dst_file_path = self.from_path(dst_file_path)
2172
- if not force and dst_file_path.exists() and is_same_file(
2196
+
2197
+ if force:
2198
+ pass
2199
+ elif not overwrite and dst_file_path.exists():
2200
+ continue
2201
+ elif dst_file_path.exists() and is_same_file(
2173
2202
  src_file_path.stat(), dst_file_path.stat(), 'copy'):
2174
2203
  continue
2204
+
2175
2205
  src_file_path.copy(dst_file_path, followlinks=followlinks)
2176
2206
 
2177
2207
  def symlink(self, dst_path: PathLike) -> None:
@@ -2181,7 +2211,7 @@ class S3Path(URIPath):
2181
2211
  :param dst_path: Desination path
2182
2212
  :raises: S3NameTooLongError, S3BucketNotFoundError, S3IsADirectoryError
2183
2213
  '''
2184
- if len(str(self._s3_path).encode()) > 1024:
2214
+ if len(fspath(self._s3_path).encode()) > 1024:
2185
2215
  raise S3NameTooLongError('File name too long: %r' % dst_path)
2186
2216
  src_bucket, src_key = parse_s3_url(self.path_with_protocol)
2187
2217
  dst_bucket, dst_key = parse_s3_url(dst_path)
megfile/sftp.py CHANGED
@@ -162,24 +162,30 @@ def sftp_realpath(path: PathLike) -> str:
162
162
  return SftpPath(path).realpath()
163
163
 
164
164
 
165
- def sftp_rename(src_path: PathLike, dst_path: PathLike) -> 'SftpPath':
165
+ def sftp_rename(
166
+ src_path: PathLike, dst_path: PathLike,
167
+ overwrite: bool = True) -> 'SftpPath':
166
168
  '''
167
169
  rename file on sftp
168
170
 
169
171
  :param src_path: Given path
170
172
  :param dst_path: Given destination path
173
+ :param overwrite: whether or not overwrite file when exists
171
174
  '''
172
- return SftpPath(src_path).rename(dst_path)
175
+ return SftpPath(src_path).rename(dst_path, overwrite)
173
176
 
174
177
 
175
- def sftp_move(src_path: PathLike, dst_path: PathLike) -> 'SftpPath':
178
+ def sftp_move(
179
+ src_path: PathLike, dst_path: PathLike,
180
+ overwrite: bool = True) -> 'SftpPath':
176
181
  '''
177
182
  move file on sftp
178
183
 
179
184
  :param src_path: Given path
180
185
  :param dst_path: Given destination path
186
+ :param overwrite: whether or not overwrite file when exists
181
187
  '''
182
- return SftpPath(src_path).replace(dst_path)
188
+ return SftpPath(src_path).replace(dst_path, overwrite)
183
189
 
184
190
 
185
191
  def sftp_remove(path: PathLike, missing_ok: bool = False) -> None:
@@ -369,7 +375,8 @@ def sftp_copy(
369
375
  src_path: PathLike,
370
376
  dst_path: PathLike,
371
377
  callback: Optional[Callable[[int], None]] = None,
372
- followlinks: bool = False):
378
+ followlinks: bool = False,
379
+ overwrite: bool = True):
373
380
  """
374
381
  Copy the file to the given destination path.
375
382
 
@@ -381,19 +388,21 @@ def sftp_copy(
381
388
  :raises IsADirectoryError: If the source is a directory.
382
389
  :raises OSError: If there is an error copying the file.
383
390
  """
384
- return SftpPath(src_path).copy(dst_path, callback, followlinks)
391
+ return SftpPath(src_path).copy(dst_path, callback, followlinks, overwrite)
385
392
 
386
393
 
387
394
  def sftp_sync(
388
395
  src_path: PathLike,
389
396
  dst_path: PathLike,
390
397
  followlinks: bool = False,
391
- force: bool = False):
398
+ force: bool = False,
399
+ overwrite: bool = True):
392
400
  '''Copy file/directory on src_url to dst_url
393
401
 
394
402
  :param src_path: Given path
395
403
  :param dst_url: Given destination path
396
404
  :param followlinks: False if regard symlink as file, else True
397
- :param force: Sync file forcely, do not ignore same files
405
+ :param force: Sync file forcely, do not ignore same files, priority is higher than 'overwrite', default is False
406
+ :param overwrite: whether or not overwrite file when exists, default is True
398
407
  '''
399
- return SftpPath(src_path).sync(dst_path, followlinks, force)
408
+ return SftpPath(src_path).sync(dst_path, followlinks, force, overwrite)
megfile/sftp_path.py CHANGED
@@ -14,6 +14,7 @@ from urllib.parse import urlsplit, urlunsplit
14
14
 
15
15
  import paramiko
16
16
 
17
+ from megfile.config import SFTP_MAX_RETRY_TIMES
17
18
  from megfile.errors import SameFileError, _create_missing_ok_generator, patch_method
18
19
  from megfile.interfaces import ContextIterator, FileEntry, PathLike, StatResult
19
20
  from megfile.lib.compare import is_same_file
@@ -47,7 +48,7 @@ SFTP_PRIVATE_KEY_PATH = "SFTP_PRIVATE_KEY_PATH"
47
48
  SFTP_PRIVATE_KEY_TYPE = "SFTP_PRIVATE_KEY_TYPE"
48
49
  SFTP_PRIVATE_KEY_PASSWORD = "SFTP_PRIVATE_KEY_PASSWORD"
49
50
  SFTP_MAX_UNAUTH_CONN = "SFTP_MAX_UNAUTH_CONN"
50
- MAX_RETRIES = 10
51
+ MAX_RETRIES = SFTP_MAX_RETRY_TIMES
51
52
  DEFAULT_SSH_CONNECT_TIMEOUT = 5
52
53
  DEFAULT_SSH_KEEPALIVE_INTERVAL = 15
53
54
 
@@ -416,9 +417,15 @@ def sftp_download(
416
417
  src_url: PathLike,
417
418
  dst_url: PathLike,
418
419
  callback: Optional[Callable[[int], None]] = None,
419
- followlinks: bool = False):
420
+ followlinks: bool = False,
421
+ overwrite: bool = True):
420
422
  '''
421
- File download
423
+ Downloads a file from sftp to local filesystem.
424
+ :param src_url: source sftp path
425
+ :param dst_url: target fs path
426
+ :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
427
+ :param followlinks: False if regard symlink as file, else True
428
+ :param overwrite: whether or not overwrite file when exists, default is True
422
429
  '''
423
430
  from megfile.fs import is_fs
424
431
  from megfile.fs_path import FSPath
@@ -428,6 +435,10 @@ def sftp_download(
428
435
  if not is_sftp(src_url) and not isinstance(src_url, SftpPath):
429
436
  raise OSError(f'src_url is not sftp path: {src_url}')
430
437
 
438
+ dst_path = FSPath(dst_url)
439
+ if not overwrite and dst_path.exists():
440
+ return
441
+
431
442
  if isinstance(src_url, SftpPath):
432
443
  src_path = src_url
433
444
  else:
@@ -440,7 +451,6 @@ def sftp_download(
440
451
  if str(dst_url).endswith('/'):
441
452
  raise IsADirectoryError('Is a directory: %r' % dst_url)
442
453
 
443
- dst_path = FSPath(dst_url)
444
454
  dst_path.parent.makedirs(exist_ok=True)
445
455
 
446
456
  sftp_callback = None
@@ -466,9 +476,14 @@ def sftp_upload(
466
476
  src_url: PathLike,
467
477
  dst_url: PathLike,
468
478
  callback: Optional[Callable[[int], None]] = None,
469
- followlinks: bool = False):
479
+ followlinks: bool = False,
480
+ overwrite: bool = True):
470
481
  '''
471
- File upload
482
+ Uploads a file from local filesystem to sftp server.
483
+ :param src_url: source fs path
484
+ :param dst_url: target sftp path
485
+ :param callback: Called periodically during copy, and the input parameter is the data size (in bytes) of copy since the last call
486
+ :param overwrite: whether or not overwrite file when exists, default is True
472
487
  '''
473
488
  from megfile.fs import is_fs
474
489
  from megfile.fs_path import FSPath
@@ -490,6 +505,9 @@ def sftp_upload(
490
505
  dst_path = dst_url
491
506
  else:
492
507
  dst_path = SftpPath(dst_url)
508
+ if not overwrite and dst_path.exists():
509
+ return
510
+
493
511
  dst_path.parent.makedirs(exist_ok=True)
494
512
 
495
513
  sftp_callback = None
@@ -858,53 +876,57 @@ class SftpPath(URIPath):
858
876
  def _is_same_protocol(self, path):
859
877
  return is_sftp(path)
860
878
 
861
- def rename(self, dst_path: PathLike) -> 'SftpPath':
879
+ def rename(self, dst_path: PathLike, overwrite: bool = True) -> 'SftpPath':
862
880
  '''
863
881
  rename file on sftp
864
882
 
865
883
  :param dst_path: Given destination path
884
+ :param overwrite: whether or not overwrite file when exists
866
885
  '''
867
886
  if not self._is_same_protocol(dst_path):
868
887
  raise OSError('Not a %s path: %r' % (self.protocol, dst_path))
869
- if str(dst_path).endswith('/'):
870
- raise IsADirectoryError('Is a directory: %r' % dst_path)
871
888
 
872
- dst_path = self.from_path(dst_path)
889
+ dst_path = self.from_path(str(dst_path).rstrip('/'))
890
+
873
891
  src_stat = self.stat()
874
892
 
875
893
  if self._is_same_backend(dst_path):
876
- try:
894
+ if overwrite:
895
+ dst_path.remove(missing_ok=True)
877
896
  self._client.rename(self._real_path, dst_path._real_path)
878
- except OSError:
879
- if dst_path.exists():
880
- raise FileExistsError('File exists: %s' % dst_path)
897
+ else:
898
+ self.sync(dst_path, overwrite=overwrite)
899
+ self.remove(missing_ok=True)
881
900
  else:
882
901
  if self.is_dir():
883
902
  for file_entry in self.scandir():
884
903
  self.from_path(file_entry.path).rename(
885
904
  dst_path.joinpath(file_entry.name))
905
+ self._client.rmdir(self._real_path)
886
906
  else:
887
- with self.open('rb') as fsrc:
888
- with dst_path.open('wb') as fdst:
889
- length = 16 * 1024
890
- while True:
891
- buf = fsrc.read(length)
892
- if not buf:
893
- break
894
- fdst.write(buf)
907
+ if overwrite or not dst_path.exists():
908
+ with self.open('rb') as fsrc:
909
+ with dst_path.open('wb') as fdst:
910
+ length = 16 * 1024
911
+ while True:
912
+ buf = fsrc.read(length)
913
+ if not buf:
914
+ break
915
+ fdst.write(buf)
895
916
  self.unlink()
896
917
 
897
918
  dst_path.utime(src_stat.st_atime, src_stat.st_mtime)
898
919
  dst_path.chmod(src_stat.st_mode)
899
920
  return dst_path
900
921
 
901
- def replace(self, dst_path: PathLike) -> 'SftpPath':
922
+ def replace(self, dst_path: PathLike, overwrite: bool = True) -> 'SftpPath':
902
923
  '''
903
924
  move file on sftp
904
925
 
905
926
  :param dst_path: Given destination path
927
+ :param overwrite: whether or not overwrite file when exists
906
928
  '''
907
- return self.rename(dst_path=dst_path)
929
+ return self.rename(dst_path=dst_path, overwrite=overwrite)
908
930
 
909
931
  def remove(self, missing_ok: bool = False) -> None:
910
932
  '''
@@ -1234,7 +1256,8 @@ class SftpPath(URIPath):
1234
1256
  self,
1235
1257
  dst_path: PathLike,
1236
1258
  callback: Optional[Callable[[int], None]] = None,
1237
- followlinks: bool = False):
1259
+ followlinks: bool = False,
1260
+ overwrite: bool = True):
1238
1261
  """
1239
1262
  Copy the file to the given destination path.
1240
1263
 
@@ -1257,6 +1280,9 @@ class SftpPath(URIPath):
1257
1280
  raise IsADirectoryError(
1258
1281
  'Is a directory: %r' % self.path_with_protocol)
1259
1282
 
1283
+ if not overwrite and self.from_path(dst_path).exists():
1284
+ return
1285
+
1260
1286
  self.from_path(os.path.dirname(dst_path)).makedirs(exist_ok=True)
1261
1287
  dst_path = self.from_path(dst_path)
1262
1288
  if self._is_same_backend(dst_path):
@@ -1291,12 +1317,14 @@ class SftpPath(URIPath):
1291
1317
  self,
1292
1318
  dst_path: PathLike,
1293
1319
  followlinks: bool = False,
1294
- force: bool = False):
1320
+ force: bool = False,
1321
+ overwrite: bool = True):
1295
1322
  '''Copy file/directory on src_url to dst_url
1296
1323
 
1297
1324
  :param dst_url: Given destination path
1298
1325
  :param followlinks: False if regard symlink as file, else True
1299
- :param force: Sync file forcely, do not ignore same files
1326
+ :param force: Sync file forcely, do not ignore same files, priority is higher than 'overwrite', default is False
1327
+ :param overwrite: whether or not overwrite file when exists, default is True
1300
1328
  '''
1301
1329
  if not self._is_same_protocol(dst_path):
1302
1330
  raise OSError('Not a %s path: %r' % (self.protocol, dst_path))
@@ -1305,11 +1333,15 @@ class SftpPath(URIPath):
1305
1333
  self.path_with_protocol, dst_path):
1306
1334
  dst_path = self.from_path(dst_file_path)
1307
1335
  src_path = self.from_path(src_file_path)
1308
- if not force and dst_path.exists() and is_same_file(
1309
- src_path.stat(), dst_path.stat(), 'copy'):
1336
+
1337
+ if force:
1338
+ pass
1339
+ elif not overwrite and dst_path.exists():
1310
1340
  continue
1311
- self.from_path(os.path.dirname(dst_file_path)).mkdir(
1312
- parents=True, exist_ok=True)
1341
+ elif dst_path.exists() and is_same_file(src_path.stat(),
1342
+ dst_path.stat(), 'copy'):
1343
+ continue
1344
+
1313
1345
  self.from_path(src_file_path).copy(
1314
1346
  dst_file_path, followlinks=followlinks)
1315
1347