Python实现FTP
一、程序介绍:
需求:
支持多用户在线的FTP程序
要求:
1、用户加密认证
2、允许同时多用户登录
3、每个用户有自己的家目录 ,且只能访问自己的家目录
4、对用户进行磁盘配额,每个用户的可用空间不同
5、允许用户在ftp server上随意切换目录
6、允许用户查看当前目录下文件
7、允许上传和下载文件,保证文件一致性
8、文件传输过程中显示进度条
9、附加功能:支持文件的断点续传
实现功能:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
程序结构:
FTP服务端
FtpServer #服务端主目录
├── bin #启动目录
│ └── ftp_server.py #启动文件
├── conf #配置文件目录
│ ├── accounts.cfg #用户存储
│ └── settings.py #配置文件
├── core #程序主逻辑目录
│ ├── ftp_server.py #功能文件
│ └── main.py #主逻辑文件
├── home #用户家目录
│ ├── test001 #用户目录
│ └── test002 #用户目录
└── log #日志目录
FTP客户端
FtpClient #客户端主目录
└── ftp_client.py #客户端执行文件
二、流程图
三、代码
#FtpServer代码
bin/ftp_server.py
#!/usr/bin/env python #_*_coding:utf-8_*_ import os import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) from core import main if __name__ == '__main__': main.ArvgHandler()
conf/accounts.cfg
[DEFAULT] [test001] Password = 123 Quotation = 100 [test002] Password = 123 Quotation = 100
conf/settings.py
#!/usr/bin/env python #_*_coding:utf-8_*_ import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) USER_HOME = "%s/home" % BASE_DIR LOG_DIR = "%s/log" % BASE_DIR LOG_LEVEL = "DEBUG" ACCOUNT_FILE = "%s/conf/accounts.cfg" % BASE_DIR HOST = "127.0.0.1" PORT = 9999
core/ftp_server.py
#!/usr/bin/env python #_*_coding:utf-8_*_ import socketserver import json import configparser import os import hashlib from conf import settings STATUS_CODE = { 250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}", 251:"Invalid cmd", 252:"Invalid auth data", 253:"Wrong username or password", 254:"Passed authentication", 255:"filename doesn't provided", 256:"File doesn't exist on server", 257:"ready to send file", 258:"md5 verification", } ''' 250:“无效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}”, 251:“无效的CMD”, 252:“验证数据无效”, 253:“错误的用户名或密码”, 254:“通过身份验证”, 255:“文件名不提供”, 256:“服务器上不存在文件”, 257:“准备发送文件”, 258:“md5验证”, ''' class FTPHandler(socketserver.BaseRequestHandler): def handle(self): '''接收客户端消息(用户,密码,action)''' while True: self.data = self.request.recv(1024).strip() print(self.client_address[0]) print(self.data) # self.request.sendall(self.data.upper()) if not self.data: print("client closed...") break data = json.loads(self.data.decode()) #接收客户端消息 if data.get('action') is not None: #action不为空 print("---->", hasattr(self, "_auth")) if hasattr(self, "_%s" % data.get('action')): #客户端action 符合服务端action func = getattr(self, "_%s" % data.get('action')) func(data) else: #客户端action 不符合服务端action print("invalid cmd") self.send_response(251) # 251:“无效的CMD” else: #客户端action 不正确 print("invalid cmd format") self.send_response(250) # 250:“无效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}” def send_response(self,status_code,data=None): '''向客户端返回数据''' response = {'status_code':status_code,'status_msg':STATUS_CODE[status_code]} if data: response.update(data) self.request.send(json.dumps(response).encode()) def _auth(self,*args,**kwargs): '''核对服务端 发来的用户,密码''' # print("---auth",args,kwargs) data = args[0] if data.get("username") is None or data.get("password") is None: #客户端的用户和密码有一个为空 则返回错误 self.send_response(252) # 252:“验证数据无效” user = self.authenticate(data.get("username"),data.get("password")) #把客户端的用户密码进行验证合法性 if user is None: #客户端的数据为空 则返回错误 self.send_response(253) # 253:“错误的用户名或密码” else: print("password authentication",user) self.user = user self.send_response(254) # 254:“通过身份验证” def authenticate(self,username,password): '''验证用户合法性,合法就返回数据,核对本地数据''' config = configparser.ConfigParser() config.read(settings.ACCOUNT_FILE) if username in config.sections(): #用户匹配成功 _password = config[username]["Password"] if _password == password: #密码匹配成功 print("pass auth..",username) config[username]["Username"] = username return config[username] def _put(self,*args,**kwargs): "client send file to server" data = args[0] base_filename = data.get('filename') file_obj = open(base_filename, 'wb') data = self.request.recv(4096) file_obj.write(data) file_obj.close() def _get(self,*args,**kwargs): '''get 下载方法''' data = args[0] if data.get('filename') is None: self.send_response(255) # 255:“文件名不提供”, user_home_dir = "%s/%s" %(settings.USER_HOME,self.user["Username"]) #当前连接用户的目录 file_abs_path = "%s/%s" %(user_home_dir,data.get('filename')) #客户端发送过来的目录文件 print("file abs path",file_abs_path) if os.path.isfile(file_abs_path): #客户端目录文件名 存在服务端 file_obj = open(file_abs_path,'rb') # 用bytes模式打开文件 file_size = os.path.getsize(file_abs_path) #传输文件的大小 self.send_response(257,data={'file_size':file_size}) #返回即将传输的文件大小 和状态码 self.request.recv(1) #等待客户端确认 if data.get('md5'): #有 --md5 则传输时加上加密 md5_obj = hashlib.md5() for line in file_obj: self.request.send(line) md5_obj.update(line) else: file_obj.close() md5_val = md5_obj.hexdigest() self.send_response(258,{'md5':md5_val}) print("send file done....") else: #没有 --md5 直接传输文件 for line in file_obj: self.request.send(line) else: file_obj.close() print("send file done....") else: self.send_response(256) # 256:“服务器上不存在文件”= def _ls(self,*args,**kwargs): pass def _cd(self,*args,**kwargs): pass if __name__ == '__main__': HOST, PORT = "127.0.0.1", 9999
core/main.py
#!/usr/bin/env python #_*_coding:utf-8_*_ import optparse from core.ftp_server import FTPHandler import socketserver from conf import settings class ArvgHandler(object): def __init__(self): self.parser = optparse.OptionParser() # parser.add_option("-s","--host",dest="host",help="server binding host address") # parser.add_option("-p","--port",dest="port",help="server binding port") (options, args) = self.parser.parse_args() # print("parser",options,args) # print(options.host,options.port) self.verify_args(options, args) def verify_args(self,options,args): '''校验并调用相应功能''' if hasattr(self,args[0]): func = getattr(self,args[0]) func() else: self.parser.print_help() def start(self): print('---going to start server---') server = socketserver.ThreadingTCPServer((settings.HOST, settings.PORT), FTPHandler) server.serve_forever()
#FtpClient代码
ftp_client.py
#!/usr/bin/env python #_*_coding:utf-8_*_ import socket import os import sys import optparse import json import hashlib STATUS_CODE = { 250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}", 251:"Invalid cmd", 252:"Invalid auth data", 253:"Wrong username or password", 254:"Passed authentication", 255:"filename doesn't provided", 256:"File doesn't exist on server", 257:"ready to send file", } class FTPClient(object): def __init__(self): parser = optparse.OptionParser() parser.add_option("-s","--server",dest="server",help="ftp server ip_addr") parser.add_option("-P","--port",type="int",dest="port",help="ftp server port") parser.add_option("-u","--username",dest="username",help="username") parser.add_option("-p","--password",dest="password",help="password") self.options,self.args = parser.parse_args() self.verify_args(self.options,self.args) self.make_connection() def make_connection(self): '''远程连接''' self.sock = socket.socket() self.sock.connect((self.options.server,self.options.port)) def verify_args(self,options,args): '''校验参数合法性''' if options.username is not None and options.password is not None: #用户和密码,两个都不为空 pass elif options.username is None and options.password is None: #用户和密码,两个都为空 pass else: #用户和密码,有一个为空 # options.username is None or options.password is None: #用户和密码,有一个为空 exit("Err: username and password must be provided together...") if options.server and options.port: # print(options) if options.port >0 and options.port <65535: return True else: exit("Err:host port must in 0-65535") def authenticate(self): '''用户验证,获取客户端输入信息''' if self.options.username: #有输入信息 发到远程判断 print(self.options.username,self.options.password) return self.get_auth_result(self.options.username,self.options.password) else: #没有输入信息 进入交互式接收信息 retry_count = 0 while retry_count <3: username = input("username: ").strip() password = input("password: ").strip() return self.get_auth_result(username,password) # retry_count +=1 def get_auth_result(self,user,password): '''远程服务器判断 用户,密码,action ''' data = {'action':'auth', 'username':user, 'password':password,} self.sock.send(json.dumps(data).encode()) #发送 用户,密码,action 到远程服务器 等待远程服务器的返回结果 response = self.get_response() #获取服务器返回码 if response.get('status_code') == 254: #通过验证的服务器返回码 print("Passed authentication!") self.user = user return True else: print(response.get("status_msg")) def get_response(self): '''得到服务器端回复结果,公共方法''' data = self.sock.recv(1024) data = json.loads(data.decode()) return data def interactive(self): '''交互程序''' if self.authenticate(): #认证成功,开始交互 print("--start interactive iwth u...") while True: #循环 输入命令方法 choice = input("[%s]:"%self.user).strip() if len(choice) == 0:continue cmd_list = choice.split() if hasattr(self,"_%s"%cmd_list[0]): #反射判断 方法名存在 func = getattr(self,"_%s"%cmd_list[0]) #反射 方法名 func(cmd_list) #执行方法 else: print("Invalid cmd.") def _md5_required(self,cmd_list): '''检测命令是否需要进行MD5的验证''' if '--md5' in cmd_list: return True def show_progress(self,total): '''进度条''' received_size = 0 current_percent = 0 while received_size < total: if int((received_size / total) * 100) > current_percent : print("#",end="",flush=True) current_percent = (received_size / total) * 100 new_size = yield received_size += new_size def _get(self,cmd_list): ''' get 下载方法''' print("get--",cmd_list) if len(cmd_list) == 1: print("no filename follows...") return #客户端操作信息 data_header = { 'action':'get', 'filename':cmd_list[1], } if self._md5_required(cmd_list): #命令请求里面有带 --md5 data_header['md5'] = True #将md5加入 客户端操作信息 self.sock.send(json.dumps(data_header).encode()) #发送客户端的操作信息 response = self.get_response() #接收服务端返回的 操作信息 print(response) if response["status_code"] ==257: #服务端返回的状态码是:传输中 self.sock.send(b'1') # send confirmation to server base_filename = cmd_list[1].split('/')[-1] #取出要接收的文件名 received_size = 0 #本地接收总量 file_obj = open(base_filename,'wb') #bytes模式写入 if self._md5_required(cmd_list): #命令请求里有 --md5 md5_obj = hashlib.md5() progress = self.show_progress(response['file_size']) progress.__next__() while received_size < response['file_size']: #当接收的量 小于 文件总量 就循环接收文件 data = self.sock.recv(4096) #一次接收4096 received_size += len(data) #本地接收总量每次递增 try: progress.send(len(data)) except StopIteration as e: print("100%") file_obj.write(data) #把接收的数据 写入文件 md5_obj.update(data) #把接收的数据 md5加密 else: print("--->file rece done<---") #成功接收文件 file_obj.close() #关闭文件句柄 md5_val = md5_obj.hexdigest() md5_from_server = self.get_response() #获取服务端发送的 md5 if md5_from_server['status_code'] ==258: #状态码为258 if md5_from_server['md5'] == md5_val: #两端 md5值 对比 print("%s 文件一致性校验成功!" %base_filename) # print(md5_val,md5_from_server) else: #没有md5校验 直接收文件 progress = self.show_progress(response['file_size']) progress.__next__() while received_size < response['file_size']: #当接收的量 小于 文件总量 就循环接收文件 data = self.sock.recv(4096) #一次接收4096 received_size += len(data) #本地接收总量每次递增 file_obj.write(data) #把接收的数据 写入文件 try: progress.send(len(data)) except StopIteration as e: print("100%") else: print("--->file rece done<---") #成功接收文件 file_obj.close() #关闭文件句柄 def _put(self,cmd_list): ''' put 下载方法''' print("put--", cmd_list) if len(cmd_list) == 1: print("no filename follows...") return # 客户端操作信息 data_header = { 'action': 'put', 'filename': cmd_list[1], } self.sock.send(json.dumps(data_header).encode()) # 发送客户端的操作信息 self.sock.recv(1) file_obj = open(cmd_list[1],'br') for line in file_obj: self.sock.send(line) if __name__ == '__main__': ftp = FTPClient() ftp.interactive()