FtpUitl.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import ftplib
  2. import os
  3. import time
  4. import socket
  5. class FtpOper:
  6. def __init__(self):
  7. super().__init__()
  8. self.ftp = None # type: ftplib.FTP | None
  9. self._conn = {
  10. 'host': None,
  11. 'port': None,
  12. 'user': None,
  13. 'password': None,
  14. 'timeout': 30,
  15. 'passive': True,
  16. }
  17. def connect(self, host, port=21, user=None, password=None, timeout=30, passive=True):
  18. self._conn.update({'host': host, 'port': port, 'user': user, 'password': password, 'timeout': timeout, 'passive': passive})
  19. # 关闭旧连接
  20. try:
  21. if self.ftp:
  22. try:
  23. self.ftp.quit()
  24. except Exception:
  25. try:
  26. self.ftp.close()
  27. except Exception:
  28. pass
  29. finally:
  30. self.ftp = None
  31. # 建立新连接
  32. ftp = ftplib.FTP(timeout=timeout)
  33. # 一些FTP在EPSV/PASV上有兼容性问题,先尝试被动
  34. ftp.connect(host, port, timeout=timeout)
  35. if user is not None:
  36. ftp.login(user=user, passwd=password)
  37. else:
  38. ftp.login()
  39. ftp.set_pasv(passive)
  40. # 避免中文路径问题
  41. try:
  42. ftp.encoding = 'utf-8'
  43. except Exception:
  44. pass
  45. self.ftp = ftp
  46. return self.ftp
  47. def ensure_connected(self):
  48. if not self.ftp:
  49. if not self._conn['host']:
  50. raise RuntimeError('FTP未初始化连接参数')
  51. return self.connect(**self._conn)
  52. try:
  53. self.ftp.voidcmd('NOOP')
  54. return self.ftp
  55. except Exception:
  56. return self._reconnect()
  57. def _reconnect(self):
  58. # 简单重连退避
  59. last_exc = None
  60. for delay in (0.2, 0.5, 1.0, 2.0):
  61. try:
  62. return self.connect(**self._conn)
  63. except Exception as e:
  64. last_exc = e
  65. time.sleep(delay)
  66. raise last_exc
  67. def close(self):
  68. try:
  69. if self.ftp:
  70. try:
  71. self.ftp.quit() # 关闭服务器
  72. except Exception:
  73. self.ftp.close()
  74. finally:
  75. self.ftp = None
  76. def _change_dir(self, remotepath, create=False):
  77. ftp = self.ensure_connected()
  78. # 标准化路径分隔符
  79. path = (remotepath or '/').replace('\\', '/').strip()
  80. if not path:
  81. path = '/'
  82. if path == '/':
  83. ftp.cwd('/')
  84. return
  85. if not create:
  86. ftp.cwd(path)
  87. return
  88. # 递归创建
  89. if path.startswith('/'):
  90. ftp.cwd('/')
  91. parts = [p for p in path.split('/') if p]
  92. else:
  93. parts = [p for p in path.split('/') if p]
  94. for part in parts:
  95. try:
  96. ftp.cwd(part)
  97. except ftplib.error_perm:
  98. ftp.mkd(part)
  99. ftp.cwd(part)
  100. # 上传文件(带重试与自动创建目录)
  101. def uploadfile(self, localfile, remotepath):
  102. if not os.path.isfile(localfile):
  103. raise FileNotFoundError(f'本地文件不存在: {localfile}')
  104. filename = os.path.split(localfile)[-1]
  105. attempts = 0
  106. last_exc = None
  107. while attempts < 2:
  108. attempts += 1
  109. try:
  110. self._change_dir(remotepath, create=True)
  111. with open(localfile, 'rb') as file:
  112. # 使用较小块以降低长连接压力
  113. self.ftp.storbinary(f'STOR {filename}', file, blocksize=64 * 1024)
  114. return
  115. except (ftplib.error_temp, ftplib.error_reply, OSError, socket.timeout) as e:
  116. last_exc = e
  117. # 421/超时尝试重连后再试一次
  118. try:
  119. self._reconnect()
  120. except Exception:
  121. pass
  122. # 最终失败抛出
  123. if last_exc:
  124. raise last_exc
  125. # 上传文件夹
  126. def uploaddir(self, localdir, remotepath):
  127. if not os.path.isdir(localdir):
  128. raise NotADirectoryError(f'不是文件夹: {localdir}')
  129. dirname = os.path.split(localdir)[-1]
  130. new_remotepath = (remotepath.rstrip('/') + '/' + dirname).replace('//', '/')
  131. # 确保目录存在
  132. self._change_dir(new_remotepath, create=True)
  133. for entry in os.listdir(localdir):
  134. src = os.path.join(localdir, entry)
  135. if os.path.isfile(src):
  136. self.uploadfile(src, new_remotepath)
  137. elif os.path.isdir(src):
  138. self.uploaddir(src, new_remotepath)
  139. self.ensure_connected().cwd('..')
  140. # 创建文件夹(兼容原有调用)
  141. def makedir(self, dirname, remotepath, new_remotepath):
  142. try:
  143. self._change_dir(new_remotepath, create=True)
  144. print('文件夹已存在或创建成功')
  145. return '文件夹已存在'
  146. except ftplib.error_perm as ex:
  147. print(f'创建文件夹失败:{ex}')
  148. return '创建文件夹失败'
  149. # 下载文件
  150. def downloadfile(self, localfile, remotefile):
  151. self.ensure_connected()
  152. remotepath, remotefile_name = os.path.split(remotefile)
  153. if self.is_exist(remotepath, remotefile_name): # 判断文件是否存在
  154. with open(localfile, 'wb') as file:
  155. self.ftp.retrbinary(f'RETR {remotefile_name}', file.write, blocksize=64 * 1024)
  156. print(f'文件下载成功:{localfile}')
  157. else:
  158. print('文件不存在')
  159. # 下载文件夹
  160. def downloaddir(self, localdir, remotepath):
  161. self.ensure_connected()
  162. if not self.is_exist(remotepath):
  163. print('远程文件夹不存在')
  164. else:
  165. if not os.path.exists(localdir):
  166. print(f'创建本地文件夹:{localdir}')
  167. os.makedirs(localdir)
  168. self.ftp.cwd(remotepath)
  169. remotenames = self.ftp.nlst()
  170. for file in remotenames:
  171. localfile = os.path.join(localdir, file)
  172. if '.' not in file: # 简单判断文件夹
  173. if not os.path.exists(localfile):
  174. os.makedirs(localfile)
  175. self.downloaddir(localfile, remotepath + '/' + file)
  176. else:
  177. self.downloadfile(localfile, remotepath + '/' + file)
  178. self.ftp.cwd('..')
  179. # 判断文件/文件夹是否存在
  180. def is_exist(self, remotepath, filename=None):
  181. try:
  182. self._change_dir(remotepath, create=False)
  183. except ftplib.error_perm:
  184. print('远程路径不存在')
  185. return False
  186. if filename is not None:
  187. filelist = self.ftp.nlst()
  188. if filename in filelist:
  189. print(f'存在该文件{filename}')
  190. return True
  191. else:
  192. print(f'没有该文件{filename}')
  193. return False
  194. else:
  195. print(f'远程路径存在{remotepath}')
  196. return True
  197. # 删除文件
  198. def deletfile(self, filename, remotepath):
  199. self.ensure_connected()
  200. if self.is_exist(remotepath, filename):
  201. self.ftp.delete(filename)
  202. print('远程文件已删除')
  203. else:
  204. print('远程文件不存在')
  205. # 删除文件夹
  206. def deletedir(self, remotepath):
  207. self.ensure_connected()
  208. if self.is_exist(remotepath):
  209. filelist = self.ftp.nlst()
  210. if len(filelist) != 0:
  211. for file in filelist:
  212. new_remotepath = os.path.join(remotepath, file).replace('\\', '/')
  213. if '.' not in file:
  214. self.deletedir(new_remotepath)
  215. else:
  216. self.deletfile(file, remotepath)
  217. self.ftp.rmd(remotepath)
  218. print('远程文件夹删除成功')
  219. else:
  220. print('远程文件夹不存在')
  221. # 重命名
  222. def rename(self, oldname, newname):
  223. self.ensure_connected()
  224. try:
  225. self.ftp.rename(oldname, newname)
  226. return True
  227. except ftplib.error_perm:
  228. return False