- 功能要求
1. 用户加密认证
2. 服务端采用 SocketServer实现,支持多客户端连接
3. 每个用户有自己的家目录且只能访问自己的家目录
4. 对用户进行磁盘配额、不同用户配额可不同
5. 用户可以登陆server后,可切换目录
6. 能查看当前目录下文件
7. 上传下载文件,保证文件一致性
8. 传输过程中实现进度条展示
9.用户可在自己家目录进行创建目录、文件、删除目录及文件
10.服务端可实现增加用户、删除用户
11.支持上传时断点续传
- 应用知识点
a) 类的应用
b) 函数的使用
c) 多进程
d) 反射
e) socket、socketserver、hashlib、configparser、logging
f) 文件的读写
- 开发环境
- python 3.6.1
- PyCharm 2016.2.3
- 目录结构
FTPClient
|--bin (主接口目录)
|--ftpclient.py (客户端主程序接口文件)
|--config (配置文件目录)
|--code.py (状态码文件)
|--settings.py (配置文件)
|--template.py (模板文件)
|--download (下载存放目录)
|--lib (模块目录)
|--client.py (客户端各类接口封装)
|--common.py (公共接口)
|--logs (日志目录)
|--ftpclient.log (日志文件)
|--clientRun.py (主执行程序)
FTPServer
|--bin (主接口目录)
|--ftpserver.py (服务端socket接口文件)
|--main.py (主程序接口文件)
|--config (配置目录)
|--settings.py (配置文件)
|--template.py (模板文件)
|--database (数据保存目录)
|--user.ini (用户信息文件)
|--dbhelper (数据目录)
|--dbapi.py (数据操作接口)
|--lib (模块目录)
|--user.py (用户类文件用来实例化对象)
|--server.py (服务端模块,各类所有命令方法)
|--common.py (公共模块文件)
|--logs
|--ftpserver.log (日志文件)
|--upload (上传文件存放的目录)
|--serverRun.py (主执行程序)
- 模块功能系统图
1、思维导图
2、功能接口关系图
客户端:
服务端:
- 关键代码段
1、服务端
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 6 import socketserver,os,sys 7 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 8 from config import settings,template 9 from lib import common,server 10 11 12 13 logger = common.Logger('ftpserver').getlog() 14 15 class MyServer(socketserver.BaseRequestHandler): 16 17 def handle(self): 18 try: 19 client_socket = self.request 20 client_addr = self.client_address 21 logger.info("client {0} connected".format(client_addr)) 22 #发送成功标识给客户端 23 client_socket.sendall(bytes("OK",encoding='utf-8')) 24 client_user = None 25 26 while True: 27 #获取客户端命令 28 ret_client_data = str(client_socket.recv(1024),encoding='utf-8') 29 30 #判断客户端是否退出 31 if ret_client_data == b'': 32 logger.info("client {0} is exit".format(client_addr)) 33 client_socket.close() 34 35 #取出客户端命令 36 cmd = ret_client_data.split("|")[0] 37 38 logger.info("client {0} send command {1}".format(client_addr,cmd)) 39 #判断是否登录认证状态 40 if cmd == 'auth': 41 client_user = server.client_auth(client_socket, ret_client_data) 42 else: 43 try: 44 #通过反射寻找模块的命令 45 if hasattr(server,cmd): 46 func = getattr(server,cmd) 47 func(client_socket, client_user, ret_client_data) 48 else: 49 logger.error("command {0} not found".format(cmd)) 50 except Exception as e: 51 logger.error(e) 52 client_socket.close() 53 54 except Exception as e: 55 logger.error(e) 56 57 def process(): 58 """ 59 启动服务 60 :return: 61 """ 62 server = socketserver.ThreadingTCPServer((settings.FTP_SERVER_IP,settings.FTP_SERVER_PORT),MyServer) 63 server.serve_forever()
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 6 import os,sys,configparser 7 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 8 from config import settings 9 10 def readall_sections(): 11 """ 12 读取user.ini文件所有的用户名 13 :return: 返回所有的用户名列表 14 """ 15 con = configparser.ConfigParser() 16 con.read(settings.USER_INI, encoding='utf-8') 17 result = con.sections() 18 return result 19 20 def GetValue(key,value): 21 """ 22 获取user.ini文件键名值 23 :param key: 键名 24 :param value: 键值 25 :return: 26 """ 27 con = configparser.ConfigParser() 28 con.read(settings.USER_INI, encoding='utf-8') 29 result = con.get(key,value) 30 return result 31 32 def CheckSections(sections_name): 33 """ 34 检查sections项名是否存在 35 :param sections_name: 用户名 36 :return: 37 """ 38 con = configparser.ConfigParser() 39 con.read(settings.USER_INI, encoding='utf-8') 40 result = con.has_section(sections_name) 41 return result 42 43 def AddOption(sections_name, **args): 44 """ 45 添加用户信息 46 :param sections_name:用户名 47 :param args: 字典格式:('test3',password='aa',totalspace='bb',userspace='cc') 48 :return: 49 """ 50 con = configparser.ConfigParser() 51 with open(settings.USER_INI,'a+',encoding='utf-8') as f: 52 con.add_section(sections_name) 53 for key in args: 54 con.set(sections_name, key, args[key]) 55 con.write(f) 56 57 def DelSections(sections_name): 58 """ 59 删除用户信息 60 :param sections_name: 61 :return: 62 """ 63 con = configparser.ConfigParser() 64 con.read(settings.USER_INI, encoding='utf-8') 65 with open(settings.USER_INI,'w') as f: 66 con.remove_section(sections_name) 67 con.write(f) 68 69 def ModifyOption(sections_name, **args): 70 """ 71 修改磁盘配额空间 72 :param sections_name: 用户名 73 :param args:用户字典信息 74 :return: 75 """ 76 con = configparser.ConfigParser() 77 con.read(settings.USER_INI, encoding='utf-8') 78 for key in args: 79 con.set(sections_name, key, args[key]) 80 with open(settings.USER_INI, 'w', encoding='utf-8') as f: 81 con.write(f) 82 83 def load_info(sections_name): 84 """ 85 加载用户信息 86 :param sections_name: 用户名 87 :return: 返回字典用户信息 88 """ 89 con = configparser.ConfigParser() 90 con.read(settings.USER_INI, encoding='utf-8') 91 user_dict = {} 92 for i, j in con.items(sections_name): 93 user_dict[i] = j 94 return user_dict
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 6 import os,sys,time 7 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 8 from lib.common import Logger 9 from lib.user import Users 10 from lib import common 11 12 13 logger = Logger('serverr').getlog() 14 15 16 17 def client_auth(client_socket,args): 18 19 """ 20 客户端认证 21 :param client_socket: 客户端socket对象 22 :param args: 用户发送过来的数据 ex: "auth|test|a7470858e79c282bc2f6adfd831b132672dfd1224c1e78cbf5bcd057" 23 :return: success:认证成功;user_error:用户名不存在;fail:认证失败 24 """ 25 recv_data_list = args.split("|") 26 username = recv_data_list[1] 27 passwd = recv_data_list[2] 28 client_user = Users(username) 29 #判断用户名是否存在 30 if client_user.check_user(): 31 msg = client_user.load_user_info() 32 password,totalspace,userspace = msg.strip().split("|") 33 user_info = "{0}|{1}".format(totalspace, userspace) 34 #判断密码是否正确 35 if password == passwd: 36 auth_status = "success" 37 else: 38 auth_status = "fail" 39 else: 40 auth_status = "user_error" 41 42 #将认证状态发送给客户端 43 client_socket.sendall(bytes(auth_status,encoding='utf-8')) 44 if auth_status == "success": 45 # 认证成功将用户空间消息发给客户端 46 client_socket.sendall(bytes(user_info, encoding='utf-8')) 47 return client_user 48 49 50 def cd(client_socket,client_user,ret_data): 51 """ 52 切换目录路径 53 :param client_socket: 客户端socket对象 54 :param client_user: 客户端用户对象 55 :param ret_data: 接收客户命令消息体 例如:cd|..或cd|test或cd|/test/aa/bb 56 :return: 57 """ 58 #获取命令行消息体 59 cd_folder = ret_data.split("|")[1] 60 try: 61 #判断是否当前根目录 62 if cd_folder == "..": 63 if client_user.userpath == client_user.homepath: 64 sed_msg = "0|{0}".format(os.path.basename(client_user.userpath)) 65 else: 66 #返回上一级目录 67 client_user.userpath = os.path.dirname(client_user.userpath) 68 sed_msg = "1|{0}".format(os.path.basename(client_user.userpath)) 69 elif cd_folder == "." or cd_folder == "": 70 sed_msg = "3|{0}".format(cd_folder) 71 else: 72 #组合路径目录 73 tmp_path = os.path.join(client_user.userpath, cd_folder) 74 if os.path.isdir(tmp_path): 75 client_user.userpath = tmp_path 76 sed_msg = "1|{0}".format(os.path.basename(client_user.userpath)) 77 else: 78 # 不是文件夹 79 sed_msg = "2|{0}".format(cd_folder) 80 # 开始发送结果 81 client_socket.sendall(bytes(sed_msg,encoding='utf-8')) 82 except Exception as e: 83 logger.error(e) 84 85 def put(client_socket,client_user,ret_data): 86 """ 87 上传文件 88 :param client_socket: 89 :param client_user: 90 :param ret_data: 91 :return: 92 """ 93 # 初始化上传文件的基本信息 94 filename = ret_data.split("|")[1] 95 filesize = int(ret_data.split("|")[2]) 96 filemd5 = ret_data.split("|")[3] 97 put_folder = client_user.userpath 98 check_filename = os.path.isfile(os.path.join(put_folder,filename)) 99 save_path = os.path.join(put_folder, filename) 100 fmd5 = common.md5sum(save_path) 101 #不存在文件名,正常传输 102 if not check_filename: 103 client_socket.sendall(bytes("ok",encoding='utf-8')) 104 # 全新的文件的话,更新用户使用空间大小 105 client_user.update_quota(filesize) 106 # 已经接收的文件大小 107 has_recv = 0 108 with open(save_path,'wb') as f: 109 while True: 110 # 如果文件总大小等于已经接收的文件大小,则退出 111 if filesize == has_recv: 112 break 113 data = client_socket.recv(1024) 114 f.write(data) 115 has_recv += len(data) 116 else: 117 #存在文件名条件,做判断分析是否存在断点 118 if fmd5 == filemd5: 119 client_socket.sendall(bytes("full", encoding='utf-8')) 120 # 已经接收的文件大小 121 has_recv = 0 122 with open(save_path, 'wb') as f: 123 while True: 124 # 如果文件总大小等于已经接收的文件大小,则退出 125 if filesize == has_recv: 126 break 127 data = client_socket.recv(1024) 128 f.write(data) 129 has_recv += len(data) 130 else: 131 #存在断点文件,发起请求续签标志 132 recv_size = os.stat(save_path).st_size 133 ready_status = "{0}|{1}".format("continue", str(recv_size)) 134 client_socket.sendall(bytes(ready_status, encoding='utf-8')) 135 # 已经接收的文件大小 136 has_recv = 0 137 with open(save_path, 'wb') as f: 138 while True: 139 # 如果文件总大小等于已经接收的文件大小,则退出 140 if filesize == has_recv: 141 break 142 data = client_socket.recv(1024) 143 f.write(data) 144 has_recv += len(data) 145 146 def get(client_socket,client_user,ret_data): 147 """ 148 下载文件 149 :param client_socket: 150 :param client_user: 151 :param ret_data: 152 :return: 153 """ 154 # 获取文件名 155 filename = ret_data.split("|")[1] 156 # 文件存在吗 157 file = os.path.join(client_user.userpath, filename) 158 if os.path.exists(file): 159 # 先告诉客户端文件存在标识 160 client_socket.send(bytes("1", 'utf8')) 161 # 得到客户端回应 162 client_socket.recv(1024) 163 # 发送文件的基本信息 "filesize|file_name|file_md5" 164 filesize = os.stat(file).st_size 165 file_md5 = common.md5sum(file) 166 sent_data = "{fsize}|{fname}|{fmd5}".format(fsize=str(filesize), 167 fname=filename, 168 fmd5=file_md5) 169 client_socket.sendall(bytes(sent_data, 'utf8')) 170 171 # 客户端收到ready 172 if str(client_socket.recv(1024), 'utf-8') == "ready": 173 # 开始发送数据了 174 with open(file, 'rb') as f: 175 new_size = 0 176 for line in f: 177 client_socket.sendall(line) 178 new_size += len(line) 179 if new_size >= filesize: 180 break 181 else: 182 # 文件不存在 183 client_socket.send(bytes("0", 'utf8')) 184 185 186 def mk(client_socket,client_user,ret_data): 187 """ 188 创建目录 189 :param client_socket: 客户端socket对象 190 :param client_user: 客户端用户对象 191 :param ret_data: 接收客户命令消息体 例如:mk|test 192 :return: 193 """ 194 mk_folder = ret_data.split("|")[1] 195 if mk_folder: 196 try: 197 folder_path = os.path.join(client_user.homepath, mk_folder) 198 os.makedirs(folder_path) 199 client_socket.sendall(bytes("5000",encoding='utf-8')) 200 except Exception as e: 201 client_socket.sendall(bytes("5001",encoding='utf-8')) 202 logger.error("create directory failure: %s" % e) 203 else: 204 client_socket.sendall(bytes("5002", encoding='utf-8')) 205 206 def delete(client_socket,client_user,ret_data): 207 """ 208 删除目录或文件 209 :param client_socket:客户端socket对象 210 :param client_user:客户端用户对象 211 :param ret_data:接收消息体:样本格式:delete|/test/aa 212 :return: 213 """ 214 del_folder = ret_data.split("|")[1] 215 if del_folder: 216 try: 217 #判断文件名是否存在 218 folder_path = os.path.join(client_user.homepath, del_folder) 219 filesize = os.stat(folder_path).st_size 220 if os.path.isfile(folder_path): 221 os.remove(folder_path) 222 client_user.update_down_quota(filesize) 223 sent_data = "{staus}|{fsize}".format(staus="6001", 224 fsize=str(filesize) 225 ) 226 227 client_socket.sendall(bytes(sent_data, encoding='utf-8')) 228 #判断目录是否存在 229 elif os.path.isdir(folder_path): 230 os.removedirs(folder_path) 231 client_socket.sendall(bytes("6000", encoding='utf-8')) 232 else: 233 #目录或文件名不存在情况,删除失败 234 client_socket.sendall(bytes("6002", encoding='utf-8')) 235 except Exception as e: 236 #当前路径目录下不是空目录,不能删除 237 client_socket.sendall(bytes("6004", encoding='utf-8')) 238 logger.error("Delete directory or filename failure: %s" % e) 239 else: 240 #命令行后是空白目录 241 client_socket.sendall(bytes("6003", encoding='utf-8')) 242 243 244 def ls(client_socket,client_user,ret_data): 245 """ 246 显示当前文件目录及文件名 247 :param client_socket: 客户端socket对象 248 :param client_user: 客户端用户对象 249 :param ret_data: 接收消息体样本格式:ls| 250 :return: 251 """ 252 check_folder = client_user.userpath 253 #获取用户目录下的文件目录或文件名列表 254 file_list = os.listdir(check_folder) 255 #目录下的文件个数 256 file_count = len(file_list) 257 if file_count > 0: 258 return_list = "{filecount}|".format(filecount=file_count) 259 for rootpath in file_list: 260 file = os.path.join(check_folder,rootpath) 261 stat = os.stat(file) 262 create_time = time.strftime('%Y:%m-%d %X', time.localtime(stat.st_mtime)) 263 file_size = stat.st_size 264 if os.path.isfile(file): 265 return_list += "{ctime} {fsize} {fname} ".format(ctime=create_time, 266 fsize=str(file_size).rjust(10, " "), 267 fname=rootpath) 268 if os.path.isdir(file): 269 return_list += "{ctime} <DIR> {fsize} {fname} ".format(ctime=create_time, 270 fsize=str(file_size).rjust(10, " "), 271 fname=rootpath) 272 else: 273 return_list = "0|" 274 275 try: 276 # 开始发送信息到客户端 277 # 1 先把结果串的大小发过去 278 str_len = len(return_list.encode("utf-8")) 279 client_socket.sendall(bytes(str(str_len), encoding='utf-8')) 280 # 2 接收客户端 read 标识,防止粘包 281 read_stat = client_socket.recv(1024).decode() 282 if read_stat == "ready": 283 client_socket.sendall(bytes(return_list, encoding='utf-8')) 284 else: 285 logger.error("client send show command,send 'ready' status fail") 286 except Exception as e: 287 logger.error(e)
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 6 import os,sys 7 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 8 from config import settings 9 from dbhelper import dbapi 10 from lib.common import Logger 11 12 logger = Logger('user').getlog() 13 14 """ 15 服务端用户信息类 16 """ 17 18 class Users(object): 19 def __init__(self,username): 20 self.username = username 21 self.password = "" 22 self.totalspace = 0 23 self.userspace = 0 24 self.homepath = os.path.join(settings.USER_HOME_FOLDER, self.username) 25 self.userpath = self.homepath 26 27 def create_user(self): 28 """ 29 创建用户 30 :return: True:创建用户成功; False: 创建用户失败 31 """ 32 args = dict(password=str(self.password), totalspace=str(self.totalspace), userspace=str(self.userspace)) 33 dbapi.AddOption(self.username, **args) 34 self.__create_folder() 35 36 def del_user(self): 37 """ 38 删除用户 39 :return: True;删除用户成功;False: 删除用户失败 40 """ 41 dbapi.DelSections(self.username) 42 self.__del_folder() 43 44 def check_user(self): 45 """ 46 判断用户是否存在 47 :return: 48 """ 49 if dbapi.CheckSections(self.username): 50 return True 51 return False 52 53 54 def load_user_info(self): 55 """ 56 加载用户信息,赋值给属性 57 :return: 58 """ 59 user_info = dbapi.load_info(self.username) 60 self.password = user_info["password"] 61 self.totalspace = int(user_info["totalspace"]) 62 self.userspace = int(user_info["userspace"]) 63 msg = "{0}|{1}|{2}".format(self.password, self.totalspace, self.userspace) 64 return msg 65 66 def __create_folder(self): 67 """ 68 创建用户的目录 69 :return: 70 """ 71 os.mkdir(self.homepath) 72 73 def __del_folder(self): 74 """ 75 删除用户目录 76 :return: 77 """ 78 os.removedirs(self.homepath) 79 80 81 def update_quota(self,filesize): 82 """ 83 更新用户磁盘配额数据 84 :param filesize: 上传文件大小 85 :return: True: 更新磁盘配额成功;False:更新磁盘配额失败 86 """ 87 if dbapi.CheckSections(self.username): 88 self.userspace += filesize 89 args = dict(userspace=str(self.userspace)) 90 dbapi.ModifyOption(self.username, **args) 91 return True 92 return False 93 94 def update_down_quota(self,filesize): 95 """ 96 用户删除文件情况,自动减少对应文件大小并更新用户磁盘配额空间 97 :param filesize: 98 :return: 99 """ 100 if dbapi.CheckSections(self.username): 101 self.userspace -= filesize 102 args = dict(userspace=str(self.userspace)) 103 dbapi.ModifyOption(self.username, **args) 104 return True 105 return False
2、客户端
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 import os,sys 6 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 7 from config import settings,template,code 8 from lib import common 9 from lib.client import client 10 11 logger = common.Logger('ftpclient').getlog() 12 13 14 def run(): 15 common.message(template.START_MENU,"INFO") 16 common.message("正在连接服务器 {0}:{1}......".format(settings.FTP_SERVER_IP,settings.FTP_SERVER_PORT),"INFO") 17 18 #创建对象 19 client_obj = client(settings.FTP_SERVER_IP,settings.FTP_SERVER_PORT) 20 #连接服务器,返回结果 21 conn_result = client_obj.connect() 22 if conn_result == code.CONN_SUCC: 23 common.message("连接成功!", "INFO") 24 #客户端登录 25 login_result = client_obj.login() 26 if login_result: 27 exit_flag = False 28 while not exit_flag: 29 show_menu = template.LOGINED_MENU.format(client_obj.username, 30 str(int(client_obj.totalspace / 1024 / 1024)), 31 str(int(client_obj.userspace / 1024 / 1024))) 32 common.message(show_menu,"INFO") 33 inp_command = common.input_command("[请输入命令]:") 34 if inp_command == "quit": 35 exit_flag = True 36 else: 37 #获取命令 38 func = inp_command.split("|")[0] 39 try: 40 if hasattr(client, func): 41 #从模块寻找到函数 42 target_func = getattr(client, func) 43 #执行函数 44 target_func(client_obj, inp_command) 45 else: 46 common.message("Client {0} 未找到".format(inp_command), "ERROR") 47 except Exception as e: 48 logger.error(e) 49 else: 50 common.message("连接失败!", "ERROR")
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 6 import os,sys,socket 7 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 8 from config import settings 9 from config import code 10 from lib import common 11 12 logger = common.Logger('client').getlog() 13 14 class client(object): 15 def __init__(self,server_addr, server_port): 16 self.username ="" 17 self.totalspace = 0 18 self.userspace = 0 19 self.client = socket.socket() 20 self.__server = (server_addr, server_port) 21 22 def connect(self): 23 """ 24 客户端连接验证 25 :return: 连接成功返回1000;连接失败返回1001 26 """ 27 try: 28 self.client.connect(self.__server) 29 ret_bytes = self.client.recv(1024) 30 #接收服务端消息 31 ret_str = str(ret_bytes, encoding='utf-8') 32 if ret_str == "OK": 33 return code.CONN_SUCC 34 else: 35 return code.CONN_FAIL 36 except Exception as e: 37 logger.error(e) 38 39 def check_auth(self,user, passwd): 40 """ 41 客户端状态发送给服务端验证,并返回结果 42 :param user: 用户名 43 :param passwd: 密码 44 :return: 45 """ 46 sendmsg = "{cmd}|{user}|{passwd}".format(cmd="auth", 47 user=user, 48 passwd=passwd) 49 self.client.sendall(bytes(sendmsg,encoding='utf-8')) 50 ret_bytes = self.client.recv(1024) 51 #获取服务端返回的认证状态信息 52 auth_info = str(ret_bytes, encoding='utf-8') 53 if auth_info == "success": 54 self.username = user 55 #获取服务端返回用户空间信息 56 user_info = str(self.client.recv(1024),encoding='utf-8') 57 self.totalspace = int(user_info.split("|")[0]) 58 self.userspace = int(user_info.split("|")[1]) 59 return code.AUTH_SUCC 60 if auth_info == "user_error": 61 return code.AUTH_USER_ERROR 62 if auth_info == "fail": 63 return code.AUTH_FAIL 64 65 def login(self): 66 while True: 67 username = str(input("请输入用户名:")).strip() 68 password = str(input("请输入密码:")).strip() 69 #对密码进行md5加密 70 password = common.md5(password) 71 #登录认证 72 auth_status = self.check_auth(username,password) 73 if auth_status == code.AUTH_SUCC: 74 common.message(">>>>>>>登录成功","INFO") 75 return True 76 elif auth_status == code.AUTH_USER_ERROR: 77 common.message(">>>>>>>用户名不存在","ERROR") 78 return False 79 else: 80 common.message(">>>>>>>用户名或密码错误!","ERROR") 81 return False 82 83 def mk(self,command): 84 """ 85 创建目录 86 :param command: 发送命令消息格式;mk|test或mk|/test/yj 87 :return: 88 """ 89 #发送命令消息给服务端 90 self.client.sendall(bytes(command,encoding='utf-8')) 91 #接收服务端发来的回应消息 92 mk_msg = str(self.client.recv(1024), encoding='utf-8') 93 mk_msg = int(mk_msg) 94 if mk_msg == code.FILE_MK_SUCC: 95 common.message(">>>>>>>创建目录成功","INFO") 96 elif mk_msg == code.FILE_MK_FAIL: 97 common.message(">>>>>>>创建目录失败","ERROR") 98 else: 99 common.message(">>>>>>>请输入文件夹名","ERROR") 100 101 def delete(self,command): 102 """ 103 删除目录或文件名 104 :param command: delete|PycharmProjects/untitled/project/FTPv1/FTPServer/upload/admin/test/aa 105 :return: 106 """ 107 # 发送命令消息给服务端 108 self.client.sendall(bytes(command, encoding='utf-8')) 109 # 接收服务端发来的回应消息 110 del_msg = str(self.client.recv(1024), encoding='utf-8') 111 reve_status = int(del_msg.split("|")[0]) 112 reve_delfilename_fsize = int(del_msg.split("|")[1]) 113 114 if del_msg == code.FOLDER_DEL_SUCC: 115 common.message(">>>>>>>删除目录成功","INFO") 116 elif reve_status == code.FILE_DEL_SUCC: 117 #更新用户空间配额大小 118 self.userspace -= reve_delfilename_fsize 119 common.message(">>>>>>>删除文件名成功","INFO") 120 elif reve_status == code.FILE_DEL_FAIL: 121 common.message(">>>>>>>删除目录或文件名失败","ERROR") 122 elif reve_status == code.FILE_DEL_EMPTY: 123 common.message(">>>>>>>当前目录下不是空目录,无法删除!","ERROR") 124 else: 125 common.message(">>>>>>>命令行请输入需要删除的路径目录或文件名!","ERROR") 126 127 def cd(self,command): 128 """ 129 切换目录路径 130 :param command: cd|.. 或cd|foldername 131 :return: 返回状态信息 132 """ 133 # 发送命令消息给服务端 134 self.client.sendall(bytes(command, encoding='utf-8')) 135 # 接收服务端发来的回应消息 136 cd_msg = str(self.client.recv(1024), encoding='utf-8') 137 result_status,result_folder = cd_msg.split("|") 138 if result_status == "0": 139 result_value = "当前是根目录" 140 elif result_status == "1": 141 result_value = "目录已切换到:{0}".format(result_folder) 142 elif result_status == "2": 143 result_value = "切换失败, {0} 不是一个目录".format(result_folder) 144 elif result_status == "3": 145 result_value = "命令无效:{0}".format(result_folder) 146 common.message(result_value,"INFO") 147 148 def ls(self,*args): 149 """ 150 显示客户端的文件列表详细信息 151 :param args: 152 :return: 返回文件列表 153 """ 154 try: 155 # 发送命令到服务端 156 self.client.send(bytes("ls|", encoding='utf-8')) 157 # 接收服务端发送结果的大小 158 total_data_len = self.client.recv(1024).decode() 159 # 收到了并发送一个ready标识给服务端 160 self.client.send(bytes("ready", 'utf-8')) 161 162 # 开始接收数据 163 total_size = int(total_data_len) # 文件总大小 164 has_recv = 0 # 已经接收的文件大小 165 exec_result = bytes("", 'utf8') 166 while True: 167 # 如果文件总大小等于已经接收的文件大小,则退出 168 if total_size == has_recv: 169 break 170 data = self.client.recv(1024) 171 has_recv += len(data) 172 exec_result += data 173 # 获取结果中文件及文件夹的数量 174 return_result = str(exec_result, 'utf-8') 175 file_count = int(return_result.split("|")[0]) 176 if file_count == 0: 177 return_result = "目前无上传记录" 178 else: 179 return_result = return_result.split("|")[1] 180 common.message(return_result,"INFO") 181 except Exception as e: 182 logger.error("client ls:{0}".format(e)) 183 184 def put(self,command): 185 """ 186 上传文件 187 :param command: put|folderfile 188 :return: 189 """ 190 file_name = command.split("|")[1] 191 if os.path.isfile(file_name): 192 filename = os.path.basename(file_name) 193 fsize = os.stat(file_name).st_size 194 fmd5 = common.md5sum(file_name) 195 196 # 将基本信息发给服务端 197 file_msg = "{cmd}|{file}|{filesize}|{filemd5}".format(cmd='put', 198 file=filename, 199 filesize=fsize, 200 filemd5=fmd5) 201 self.client.send(bytes(file_msg, encoding='utf8')) 202 logger.info("send file info: {0}".format(file_msg)) 203 #接收来自服务端数据 204 put_msg = str(self.client.recv(1024),encoding='utf-8') 205 try: 206 #正常上传文件 207 if put_msg == "ok": 208 #判断是否超过用户空间配额 209 if self.userspace + fsize > self.totalspace: 210 common.message("用户磁盘空间不足,无法上传文件,请联系管理员!","ERROR") 211 else: 212 self.userspace += fsize 213 new_size = 0 214 with open(file_name,'rb') as f: 215 for line in f: 216 self.client.sendall(line) 217 new_size += len(line) 218 # 打印上传进度条 219 common.progress_bar(new_size,fsize) 220 if new_size >= fsize: 221 break 222 #断点续传文件 223 if put_msg.split("|")[0] == "continue": 224 send_size = int(put_msg.split("|")[1]) 225 common.message("服务端存在此文件,但未上传完,开始断点续传......","INFO") 226 new_size = 0 227 with open(file_name,'rb') as f: 228 #用seek来进行文件指针的偏移,实现断点续传的功能 229 f.seek(send_size) 230 while fsize - send_size > 1024: 231 revedata = f.read(1024) 232 self.client.sendall(revedata) 233 new_size += len(revedata) 234 #打印上传进度条 235 common.progress_bar(new_size, fsize) 236 else: 237 revedata = f.read(fsize - send_size) 238 self.client.sendall(revedata) 239 # 打印上传进度条 240 common.progress_bar(new_size, fsize) 241 242 #不存在断点文件情况,询问是否覆盖掉原文件 243 if put_msg == "full": 244 inp_msg = common.message("服务端存在完整文件,是否覆盖掉原文件[输入y或n]:","INFO") 245 inp = str(input(inp_msg)).strip().lower() 246 if inp == "y": 247 with open(file_name, 'rb') as f: 248 new_size = 0 249 for line in f: 250 self.client.sendall(line) 251 new_size += len(line) 252 #打印上传进度条 253 common.progress_bar(new_size, fsize) 254 if new_size >= fsize: 255 break 256 elif inp == "n": 257 sys.exit() 258 else: 259 common.message("无效命令", "ERROR") 260 logger.info("upload file<{0}> successful".format(file_name)) 261 common.message("文件上传成功", "INFO") 262 except Exception as e: 263 logger.error("文件上传失败:{0}".format(e)) 264 common.message("文件上传失败!", "ERROR") 265 else: 266 common.message("文件不存在!", "ERROR") 267 268 def get(self,command): 269 """ 270 下载文件 271 :param command: 272 :return: 273 """ 274 return_result = "" 275 # 发送基本信息到服务端 (command,username,file) 276 self.client.send(bytes(command, encoding='utf-8')) 277 # 先接收到命令是否正确标识,1 文件存在, 0 文件不存在 278 ack_by_server = self.client.recv(1024) 279 try: 280 # 文件名错误,当前路径下找不到 281 if str(ack_by_server, encoding='utf-8') == "0": 282 return_result = " 当前目录下未找到指定的文件,请到存在目录下执行get操作!" 283 else: 284 # 给服务端回应收到,防止粘包 285 self.client.send(bytes("ok", 'utf8')) 286 287 # 文件存在,开始接收文件基本信息(大小,文件名) 288 file_info = self.client.recv(1024).decode() 289 file_size = int(file_info.split("|")[0]) 290 file_name = file_info.split("|")[1] 291 file_md5 = file_info.split("|")[2] 292 293 # 2 发送 ready 标识,准备开始接收文件 294 self.client.send(bytes("ready", 'utf8')) 295 296 # 3 开始接收数据了 297 has_recv = 0 298 with open(os.path.join(settings.DOWNLOAD_FILE_PATH, file_name), 'wb') as f: 299 while True: 300 # 如果文件总大小等于已经接收的文件大小,则退出 301 if file_size == has_recv: 302 break 303 data = self.client.recv(1024) 304 f.write(data) 305 has_recv += len(data) 306 # 打印下载进度条 307 common.progress_bar(has_recv, file_size) 308 return_result = " 文件下载成功" 309 logger.info("download file<{0}> from server successful".format(file_name)) 310 # md5文件验证 311 check_md5 = common.md5sum(os.path.join(settings.DOWNLOAD_FILE_PATH, file_name)) 312 if check_md5 == file_md5: 313 logger.info("md5 check for file<{0}> succ!".format(file_name)) 314 return_result += ", MD5 验证成功! " 315 else: 316 return_result += ", MD5 验证文件不匹配! " 317 common.message(return_result,"INFO") 318 except Exception as e: 319 logger.error(e)
1 #!/usr/bin/env python 2 #coding=utf-8 3 __author__ = 'yinjia' 4 5 6 import os,sys,logging,hashlib,time 7 sys.path.append(os.path.dirname(os.path.dirname(__file__))) 8 from config import settings 9 10 class Logger(object): 11 """ 12 日志记录,写入指定日志文件 13 """ 14 def __init__(self,logger): 15 16 create_time = time.strftime('%Y-%m-%d %H:%M:%S') 17 format = '[%(name)s]:[%(asctime)s] [%(filename)s|%(funcName)s] [line:%(lineno)d] %(levelname)-8s: %(message)s' 18 19 # 创建一个logger 20 self.logger = logging.getLogger(logger) 21 self.logger.setLevel(logging.INFO) 22 23 # 创建一个handler,用于写入日志文件 24 fp = logging.FileHandler(settings.LOGS) 25 26 # 定义handler的输出格式formatter 27 fpmatter = logging.Formatter(format) 28 fp.setFormatter(fpmatter) 29 30 # 给logging添加handler 31 self.logger.addHandler(fp) 32 33 def getlog(self): 34 return self.logger 35 36 37 def md5(arg): 38 """ 39 密码进行md5加密 40 :param arg: 用户的密码 41 :return: 返回进行加密后的密码 42 """ 43 result = hashlib.md5() 44 result.update(arg.encode()) 45 return result.hexdigest() 46 47 48 def md5sum(filename): 49 """ 50 用于获取文件的md5值 51 :param filename: 文件名 52 :return: MD5码 53 """ 54 if not os.path.isfile(filename): # 如果校验md5的文件不是文件,返回空 55 return False 56 myhash = hashlib.md5() 57 f = open(filename, 'rb') 58 while True: 59 b = f.read(1024) 60 if not b: 61 break 62 myhash.update(b) 63 f.close() 64 return myhash.hexdigest() 65 66 def message(msg,type): 67 """ 68 根据不同的消息类型,打印出消息内容以不同的颜色显示 69 :param msg: 消息内容 70 :param type: 消息类型 71 :return: 返回格式化后的消息内容 72 """ 73 if type == "CRITICAL": 74 show_msg = " 33[1;33m{0} 33[0m ".format(msg) 75 elif type == "ERROR": 76 show_msg = " 33[1;31m{0} 33[0m ".format(msg) 77 elif type == "WARNING": 78 show_msg = " 33[1;32m{0} 33[0m ".format(msg) 79 elif type == "INFO": 80 show_msg = " 33[1;36m{0} 33[0m ".format(msg) 81 else: 82 show_msg = " {0} ".format(msg) 83 print(show_msg) 84 85 def progress_bar(cache, totalsize): 86 """ 87 打印进度条 88 :param cache: 缓存字节大小 89 :param totalsize: 文件总共字节 90 :return: 91 """ 92 ret = cache / totalsize 93 num = int(ret * 100) 94 view = ' %d%% |%s' % (num, num * "*") 95 sys.stdout.write(view) 96 sys.stdout.flush() 97 98 def input_command(msg): 99 flag = False 100 while not flag: 101 command_list = ["put","get","ls","cd","mk","delete","quit"] 102 command_inp = input(msg).strip() 103 if command_inp == "ls": 104 return_command = "{0}|".format(command_inp) 105 flag = True 106 elif command_inp == "quit": 107 return_command = command_inp 108 flag = True 109 else: 110 if command_inp.count("|") != 1: 111 message("输入命令不合法!","ERROR") 112 else: 113 #获取命令 114 cmd,args = command_inp.strip().lower().split("|") 115 if cmd not in command_list: 116 message("输入命令不合法!", "ERROR") 117 else: 118 return_command = "{0}|{1}".format(cmd, args) 119 flag = True 120 return return_command
- 部分效果展示图
- [备注]:完整代码详见:
Hithub: https://github.com/yingoja/FTPServer