该文档为用python3实现ftp上传下载等功能。
1 import optparse
2 import socket
3 import json,os
4 import shelve
5
6 class FtpClient(object):
7 """ftp客户端"""
8 MSG_SIZE = 1024 # 消息最长1024
9
10 def __init__(self):
11 self.username = None
12 self.terminal_display = None
13 self.shelve_obj = shelve.open(".luffy_db")
14 self.current_dir = None
15
16 parser = optparse.OptionParser()#创建parser这个对象,optparse这个模块是个类
17 parser.add_option("-s","--server", dest="server", help="ftp server ip_addr")
18 parser.add_option("-P","--port",type="int", dest="port", help="ftp server port")
19 parser.add_option("-u","--username", dest="username", help="username info")
20 parser.add_option("-p","--password", dest="password", help="password info")
21 self.options , self.args = parser.parse_args()
22
23 #print(self.options,self.args,type(self.options),self.options.server)
24 self.argv_verification()#调用检查参数合法性函数
25
26 self.make_connection()#调用建立socket连接函数
上面这段首先创建一个类。该客户端代码均在这个类中实现。
定义一个接收值的变量,然后定义初始化函数,定义几个静态属性,这几个属性有些是后面函数需要的,所以提前定义。
之后用OptionParser这个函数生成一个命令行声明。这个函数具体相关可以从网络中查到。
再之后调用两个函数,一个是检查参数合法性的,另一个是建立socket连接。
1 def argv_verification(self):
2 """检查参数合法性"""
3 if not self.options.server or not self.options.port:
4 #如果options的server参数或port参数不为真则exit()关闭并打印括号内内容。
5 exit("Error: must supply server and port parameters")
6
7
8 def make_connection(self):
9 """建立socket链接"""
10 self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
11 self.sock.connect((self.options.server,self.options.port))
12
13 def get_response(self):#接收返回值
14 """获取服务器端返回"""
15 data = self.sock.recv(self.MSG_SIZE)#接收1024个
16 return json.loads(data.decode())#返回decode之后内容
上面代码为三个函数,分别是检查合法性、创建连接、接收返回值。
1 def auth(self):
2 """用户认证"""
3 count = 0
4 while count < 3:#输入最多3次
5 username = input("username:").strip()
6 if not username:continue#如果输入不为真则继续
7 password = input("password:").strip()
8
9 cmd = {
10 'action_type':'auth',#action_type定义功能关键词,将关键词传入值
11 'username':username,#用户名
12 'password':password,#密码
13 }
14 #定义一个字典,该字典将为以后客户端与服务端通讯作为模板。
15 self.sock.send(json.dumps(cmd).encode("utf-8"))#将字典序列化然后encode,发送
16 response = self.get_response()#调用get_response函数,并赋值。该函数获取服务器端返回内容,并反序列化
17 print("response:",response)#打印得到的内容
18 if response.get('status_code') == 200:#pass auth#如果得到的内容是状态码200
19 self.username = username
20 # 定义一个数据属性,将__init__里的None赋值掉,为了以后调用方便
21 self.terminal_display = "[%s]>>:" % self.username#将用户名赋值给显示
22 self.current_dir = "\\"#设置当前目录路径,将init函数重写
23 return True
24 else:
25 print(response.get("status_msg"))#如果验证不成功打印状态码
26 count += 1
auth函数主要负责登录认证功能。注释写的蛮详细了,我就不赘述了。
1 def unfinished_file_check(self):
2 """检查shelve db ,把为正常传完的文件列表打印,按用户的指令决定是否重传"""
3 if list(self.shelve_obj.keys()):#如果shelve列表为真
4 print("-------Unfinished file list -------------")
5 for index,abs_file in enumerate(self.shelve_obj.keys()):
6 #enumerate函数将一个可迭代的容器列出它的索引
7 received_file_size = os.path.getsize(self.shelve_obj[abs_file][1])
8 #接收到的大小=getsize大小
9 print("%s. %s %s %s %s" %(index,abs_file,
10 self.shelve_obj[abs_file][0],
11 received_file_size,
12 received_file_size/self.shelve_obj[abs_file][0]*100
13 ))
14
15 while True:
16 choice = input("[select file index to re-download]").strip()
17 if not choice:continue
18 if choice == 'back':break
19 if choice.isdigit():
20 choice = int(choice)
21 if choice >= 0 and choice <= index:
22 selected_file = list(self.shelve_obj.keys())[choice]
23 already_received_size = os.path.getsize(self.shelve_obj[selected_file][1])
24
25 print("tell server to resend file ", selected_file)
26 #abs_filename + size +received_size
27 self.send_msg('re_get', file_size=self.shelve_obj[selected_file][0],
28 received_size=already_received_size,
29 abs_filename=selected_file)
30
31 response = self.get_response()
32 if response.get('status_code') == 401:#"File exist ,ready to re-send !",
33 local_filename = self.shelve_obj[selected_file][1]
34
35
36
37 f = open(local_filename,'ab')
38 total_size = self.shelve_obj[selected_file][0]
39 recv_size = already_received_size
40 current_percent = int(recv_size /total_size *100)
41 progress_generator = self.progress_bar(total_size,current_percent,current_percent)
42 progress_generator.__next__()
43 while recv_size < total_size:
44 if total_size - recv_size < 8192: # last recv
45 data = self.sock.recv(total_size - recv_size)
46 else:
47 data = self.sock.recv(8192)
48 recv_size += len(data)
49 f.write(data)
50 progress_generator.send(recv_size)
51 #progress_generator.send(received_size)
52 #print(recv_size,total_size)
53 else:
54 print("file re-get done")
55 else:
56 print(response.get("status_msg"))
断点续传
该函数为断点续传功能,改天写注释。该函数大量使用shelve。
1 def interactive(self):
2 """处理与Ftpserver的所有交互"""
3 if self.auth():
4 self.unfinished_file_check()
5
6 while True:
7 user_input = input(self.terminal_display).strip()#输入的时候显示名字后来变成路径
8 if not user_input:continue
9
10 cmd_list = user_input.split()#登录之后,将输入的内容转化成列表
11 if hasattr(self,"_%s"%cmd_list[0]):#判断输入的内容列表索引0 这个类里是否有这个方法
12 func = getattr(self,"_%s"%cmd_list[0])#获取这个方法。
13 func(cmd_list[1:])#执行这个函数,将input后面所有的内容作为列表传进去
14 #get fil1 --md5
interactive函数负责与服务端的交互。首先调用登陆验证,然后就是下载未完成文件的检查,之后是负责交互。
1 def parameter_check(self,args,min_args=None,max_args=None,exact_args=None):
2 """参数个数合法性检查
3 # args这个传进来的参数是一个列表,min_args默认值看函数的定义。
4 下面的判断如果符合一条则报错,没有符合的则正常。"""
5 if min_args:
6 if len(args) < min_args:
7 print("must provide at least %s parameters but %s received." %(min_args,len(args)))
8 return False
9 if max_args:
10 if len(args) > max_args:
11 print("need at most %s parameters but %s received." %(max_args,len(args)))
12 return False
13
14 if exact_args:
15 if len(args) != exact_args:
16 print("need exactly %s parameters but %s received." % (exact_args, len(args)))
17 return False
18
19 return True
parameter_check
parameter_check是参数的合法性检查,检查输入内容命令+参数后面的参数格式。
def send_msg(self,action_type,**kwargs ):#action_type传进来的是get,**是文件名
"""打包消息并发送到远程"""
msg_data = {
'action_type': action_type,
'fill':''
}
msg_data.update(kwargs)#将文件名这个字典作为key传到字典里,update本质是两个字典合并成一个字典。
bytes_msg = json.dumps(msg_data).encode()#将字典序列化,目的是为了将字典转化为字符串,好算长度
if self.MSG_SIZE > len(bytes_msg):#如果1024>这个序列化后的字典
msg_data['fill'] = msg_data['fill'].zfill( self.MSG_SIZE - len(bytes_msg))
bytes_msg = json.dumps(msg_data).encode()#将调整大小后的字典序列化
self.sock.send(bytes_msg)#发送
send_msg负责具体向服务端发送消息的工作。
1 def _ls(self,cmd_args):
2 """
3 display current dir's file list
4 :param cmd_args:
5 :return:
6 """
7 self.send_msg(action_type='ls')#发送消息,将ls命令通过字典发出去
8 response = self.get_response() #1024#收消息
9 print('ls recv',response)#打印
10 if response.get('status_code') == 302:#取出收到的字典中的状态码,判断是否为302
11 cmd_result_size = response.get('cmd_result_size')#收到信息获取返回值长度
12 print(cmd_result_size)#打印长度看一下
13 received_size = 0#定义一个初始值
14 cmd_result = b''#定义一个初始值
15 while received_size < cmd_result_size:#当收入大小小于实际大小
16 if cmd_result_size - received_size < 8192:#last receive#实际大小-收入大小小于8192
17 data = self.sock.recv( cmd_result_size - received_size)#继续收入上面这个差值
18 else:
19 data = self.sock.recv(8192)#入过差值大过8192 那么索性直接收8192
20 cmd_result += data#收入的文件+=recv的内容
21 received_size += len(data)#收入的长度+=recv的长度
22 else:
23 print(cmd_result.decode("gbk"))#当收入大小不小于实际大小,那么直接打印
_ls函数是负责ls命令的功能,调用send_msg发送命令并调用get_response接收返回值,期间解决了粘包的问题。
1 def _cd(self,cmd_args):
2 """change to target dir"""
3 if self.parameter_check(cmd_args, exact_args=1):#参数合法性校验
4 target_dir = cmd_args[0] #赋值
5 self.send_msg('cd',target_dir=target_dir) #发送消息
6 response = self.get_response()#接收消息
7 print(response)#打印
8 if response.get("status_code") == 350:#dir changed
9 self.terminal_display = "[/%s]" % response.get('current_dir')#修改输入端显示
10 self.current_dir = response.get('current_dir')#将current_dir赋值
_cd函数负责cd命令的处理。
1 def _get(self,cmd_args):
2 """download file from ftp server
3 1.拿到文件名
4 2.发送到远程
5 3.等待服务器返回消息
6 3.1 如果文件存在, 拿到文件大小
7 3.1.1 循环接收
8 3.2 文件如果不存在
9 print status_msg
10
11 """
12 if self.parameter_check(cmd_args,min_args=1):
13 # cmd_args这个传进来的参数是一个列表,min_args默认是1,执行判断参数合法性函数,将列表传进去
14 filename = cmd_args[0]#将参数0赋值为文件名
15 self.send_msg(action_type='get',filename=filename)#运行发送函数,将action为get等内容字典传值。
16 response = self.get_response()#接收返回值
17 if response.get('status_code') == 301:# file exist ,ready to receive
18 file_size = response.get('file_size')#取返回值里的文件大小
19 received_size = 0#先定义为0
20
21 progress_generator = self.progress_bar(file_size)
22 progress_generator.__next__()
23
24 #save to shelve db
25 file_abs_path = os.path.join(self.current_dir,filename)
26 self.shelve_obj[file_abs_path] = [file_size,"%s.download" %filename]
27
28 f = open("%s.download" %filename,"wb")#打开一个空文件
29 while received_size < file_size:#如果收到的大小小于文件大小
30 if file_size - received_size < 8192:#last recv如果文件大小-收到的大小之后小于8192
31 data = self.sock.recv( file_size - received_size )#继续接收文件大小没有收到的数量值
32 else:
33 data = self.sock.recv(8192)#如果大于8192则直接接收8192
34 received_size += len(data)#没接收一次数据,将数据大小增加
35 f.write(data)
36 progress_generator.send(received_size)#打印文件大小和接收大小
37
38 #print(received_size,file_size)
39 else:
40 print('\n')
41 print("---file [%s] recv done,received size [%s]----"%( filename,file_size))
42 del self.shelve_obj[file_abs_path]
43 f.close()#如果接收到的大小不小于文件大小,等于文件大小,那就传完了
44 os.rename("%s.download"%filename,filename)
45
46 else:
47 print(response.get('status_msg'))#打印状态码,文件不存在
_get函数负责下载相关,同样需处理粘包工作。
1 def progress_bar(self,total_size,current_percent=0,last_percent=0):
2
3 # current_percent = 0
4 # last_percent = 0
5 #received_size = 0
6 while True:
7 received_size = yield current_percent
8 current_percent = int(received_size / total_size *100)
9
10 if current_percent > last_percent:
11 print("#" * int(current_percent / 2) + "{percent}%".format(percent=current_percent), end='\r',
12 flush=True)
13 last_percent = current_percent # 把本次循环的percent赋值给last
progress_bar。进度条功能。
1 def _put(self,cmd_args):
2 """上传本地文件到服务器
3 1. 确保本地文件存在
4 2. 拿到文件名+大小,放到消息头里发给远程
5 3. 打开文件,发送内容
6 """
7 if self.parameter_check(cmd_args, exact_args=1):#参数合法性检查
8 local_file = cmd_args[0]#取文件名
9 if os.path.isfile(local_file):#判断文件存在
10 total_size = os.path.getsize(local_file)#取文件大小
11 self.send_msg('put',file_size=total_size,filename=local_file)#发送
12 f = open(local_file,'rb')
13 uploaded_size = 0
14 #last_percent = 0
15
16 progress_generator = self.progress_bar(total_size)#进度条
17 progress_generator.__next__()#next一下
18 for line in f:
19 self.sock.send(line)
20 uploaded_size += len(line)
21 # current_percent = int(uploaded_size / total_size * 100)
22 # if current_percent > last_percent:
23 # print("#"* int(current_percent/2) + "{percent}%".format(percent=current_percent),end='\r',flush=True)
24 # last_percent = current_percent #把本次循环的percent赋值给last
25 progress_generator.send(uploaded_size)
26
27 else:
28 print('\n')
29 print('file upload done'.center(50,'-'))
30 f.close()
上传功能的函数。
1 if __name__ == "__main__":
2 client = FtpClient()
3 client.interactive() #交互
执行该py文件需使用cmd,格式为 filename.py -s IP -P port执行。
中天一片无情月,是我平生不悔心