FTP:
环境:windows, python 3.5
功能:
1.用户加密认证,可自行配置家目录磁盘大小
2.多用户登陆
3.查看当前目录(家目录权限下)
4.切换目录(家目录权限下)
5.上传下载,进度条展示,MD5认证
6.下载支持断点续传(仅限传输过程中意外终断,后续支持继续传输)
结构:
ftp_client ---|
bin ---|
start_client.py ......启动客户端
conf---|
config.py ......客户端参数配置
system.ini ......客户端参数配置文件
core---|
ftp_client.py ......客户端主程序
ftp_server ---|
bin ---|
start_server.py ......启动服务端
conf---|
config.py ......服务端参数配置
system.ini ......服务端参数配置文件
core---|
ftp_server.py ......服务端主程序
cmd.py ......执行系统命令,如(mkdir,dir等)
db ---|
data.py ......存取用户数据
home ......用户家目录,每个用户生成各自姓名的家目录
功能实现:
ftp_server.py启动后进入ftp_server.py,执行类MyServer中的handle()方法等待客户端连接;
客户端通过 start_client.py 启动进入 ftp_client.py;
执行 类FtpClient 中的connect()方法建立连接;
进入 interactive()入口,首先调用authenticate()加密登陆,如未注册则调用register()注册;
然和根据输入内容,分别映射进入到相应方法中,再各个方法里;
在各个方法中,首先将action发送给服务端;
服务端根据action将映射到服务端的各个方法中,进行数据交互;
如何使用:
启动start_server.py,启动start_client.py;
首先输入用户名,如用户名未注册则提示注册,注册时需输入家目录磁盘大小,单位为 b,登陆完成,进入菜单;
根据提示输入需要进行的操作:
dir (path):展示当前目录或者展示所给路径目录,如路径超过权限则提示权限不够;
cd (path):切换目录或返回家目录,根据输入的路径切换到相应目录,格式为cd ./path,必须输入./+路径,./表示当前位置,直接cd则返回家目录
put filepath:上传文件到家目录下
get filepath:下载文件到指定目录,只允许下载家目录里存在的文件(下载前请先上传文件到家目录下),支持断点续传
ftp_client:
bin:
#!/usr/bin/env python # -*-coding:utf-8-*- # Author:zh import os import sys PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) import core core.ftp_client.run()
conf:
#!/usr/bin/env python # -*-coding:utf-8-*- # _author_=zh import os import configparser PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) class Configuration(object): def __init__(self): self.config = configparser.ConfigParser() self.name = PATH+os.sep+"conf"+os.sep+"system.ini" def init_config(self): # 初始化配置文件,ip :客户端IP,port:客户端端口 if not os.path.exists(self.name): self.config["config"] = {"ip": "localhost", "port": 1234} self.config.write(open(self.name, "w", encoding="utf-8", )) def get_config(self, head="config"): ''' 获取配置文件数据 :param head: 配置文件的section,默认取初始化文件config的数据 :return:返回head中的所有数据(列表) ''' self.init_config() # 取文件数据之前生成配置文件 self.config.read(self.name, encoding="utf-8") if self.config.has_section(head): section = self.config.sections() return self.config.items(section[0])
core:
#!/usr/bin/env python # -*-coding:utf-8-*- # Author:zh import socket import os import sys import json import hashlib from conf import config # 服务端交互传送结果 SIGN_DICT = { '801': '用户名存在', '802': '用户名不存在', '804': '路径不存在', '806': '文件超过磁盘限额', '807': '传输完毕', '808': '传输成功', '809': '传输失败', '811': '文件无权限访问', '812': '文件不存在' } class FtpClient(object): def __init__(self): self.client = socket.socket() def connect(self, ip, port): self.client.connect((ip, port)) def interactive(self): # 入口 global catalog name = self.authenticate() self.catalog = name while True: cmd = input("%s>>" % self.catalog).strip() if len(cmd) == 0: continue cmd_str = cmd.split()[0] if hasattr(self, cmd_str): func = getattr(self, cmd_str) func(cmd, name) else: print("输入格式错误") self.help() @staticmethod def help(): msg = ''' dir path(无path默认显示当前目录下文件) cd path(切换目录) get filename(下载文件) put filename(上传文件) quit 退出 ''' print(msg) def register(self, name): # 注册 name = name pwd = input("请输入密码(q退出):").strip() limit_size = input("请输入用户磁盘空间(单位:b):") if pwd.lower() == 'q': exit() hash_md5 = hashlib.md5() hash_md5.update(pwd.encode()) pwd_md5 = hash_md5.hexdigest() user_data = { "name": name, "pwd": pwd_md5, "limit_size": limit_size } self.client.send(json.dumps(user_data).encode()) def authenticate(self): # 登陆 while True: name = input("请输入用户名:").strip().lower() msg_dict = { "action": "authenticate", "username": name } self.client.send(json.dumps(msg_dict).encode()) server_response = self.client.recv(1024) server_response = json.loads(server_response.decode()) if server_response["sign"] == '802': print(SIGN_DICT['802']) print("请注册!") self.register(name) return name elif server_response["sign"] == '801': value = server_response["value"] while True: pwd = input("请输入密码:").strip() hash_md5 = hashlib.md5() hash_md5.update(pwd.encode()) pwd_md5 = hash_md5.hexdigest() if value["pwd"] == pwd_md5: print("登陆成功") return name else: print("密码错误,请重新输入") def dir(self, *args): # 查看当前目录 cmd_split = args[0] msg_dict = { "action": "dir", "command": cmd_split, "user_name": self.catalog } self.client.send(json.dumps(msg_dict).encode()) server_response = self.client.recv(1024) answer = json.loads(server_response.decode()) if type(answer) is list: print(answer[0], answer[1]) else: print(answer) def cd(self, *args): # 切换目录,不带路径则直接切换回家目录下 cmd_split = args[0].split() if len(cmd_split) > 1: dir_path = cmd_split[1] msg_dict = { "action": "cd", "dir_path": dir_path, "now_path": self.catalog } self.client.send(json.dumps(msg_dict).encode()) data = self.client.recv(1024) if data.decode() == '803': self.catalog = dir_path.replace('.', self.catalog) print("切换成功") else: print(SIGN_DICT[data.decode()]) else: self.catalog = args[1] def put(self, *args): # 上传 cmd_split = args[0].split() if len(cmd_split) > 1: filename = cmd_split[1] if os.path.isfile(filename): file_size = os.stat(filename).st_size msg_dict = { "action": "put", "filename": filename, "size": file_size, "user_name": args[1] } self.client.send(json.dumps(msg_dict).encode()) # 防止黏包,等服务器确认 server_response = (self.client.recv(1024)).decode() if server_response == '805': file = open(filename, 'rb') hash_md5 = hashlib.md5() send_data = 0 for line in file: self.client.send(line) send_data += len(line) self.progress_bar(send_data, file_size) hash_md5.update(line) else: file.close() pwd_md5 = hash_md5.hexdigest() server_answer = (self.client.recv(1024)).decode() if server_answer == '807': self.client.send(pwd_md5.encode()) put_answer = (self.client.recv(1024)).decode() print(SIGN_DICT[put_answer]) else: print(SIGN_DICT[server_response]) else: print(filename, "is not exist") else: print("请输入上传文件路径") def get(self, *args): # 下载,支持断点续传 cmd_split = args[0].split() if len(cmd_split) > 1: file_path = cmd_split[1] msg_dict = { "action": "get", "file_path": file_path, "user_name": args[1] } self.client.send(json.dumps(msg_dict).encode()) server_response = (self.client.recv(1024)).decode() if server_response == '810': self.client.send('810'.encode()) # 避免黏包 file_data = self.client.recv(1024) file_length = (json.loads(file_data.decode()))["length"] file_name = (json.loads(file_data.decode()))["file_name"] get_path = input("请输入存放路径:") if os.path.isdir(get_path): if os.path.exists(get_path+os.sep+file_name): choose = input("文件已存在,请选择(1.覆盖,2.重命名,3续传):") if choose == "1": os.remove(get_path+os.sep+file_name) exists_length = 0 get_path = get_path+os.sep+file_name elif choose == '2': exists_length = 0 get_path = get_path + os.sep + file_name+".new" elif choose == "3": exists_length = os.stat(get_path+os.sep+file_name).st_size get_path = get_path + os.sep + file_name else: exists_length = 0 get_path = get_path + os.sep + file_name self.client.send(str(exists_length).encode()) file = open(get_path, "wb") file.seek(int(exists_length)) hash_md5 = hashlib.md5() get_length = exists_length while get_length < int(file_length): data = self.client.recv(1024) file.write(data) get_length += len(data) hash_md5.update(data) self.progress_bar(get_length, int(file_length)) else: pwd_md5 = hash_md5.hexdigest() file.close() self.client.send('传输完成'.encode()) get_md5 = (self.client.recv(1024)).decode() if get_md5 == pwd_md5: print("下载成功") else: print("下载失败") os.remove(get_path) else: print("路径输入错误") else: print(SIGN_DICT[server_response]) else: print("请输入下载文件路径") @staticmethod def quit(*args): # 退出 print("%s已退出" % args[1]) exit() @staticmethod def progress_bar(send_data, all_data): # 进度条展示 # send_data:已发送的长度,all_data:总长度 ret = send_data / all_data num = int(ret * 100) view = ' [%-100s]%d%%' % ("=" * num, 100,) sys.stdout.write(view) sys.stdout.flush() def run(): client = FtpClient() conf = config.Configuration() conf_data = conf.get_config() client.connect(conf_data[0][1], int(conf_data[1][1])) client.help() client.interactive()
ftp_server:
bin
#!/usr/bin/env python # -*-coding:utf-8-*- # Author:zh import os import sys PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PATH) import core core.ftp_server.run()
conf:
#!/usr/bin/env python # -*-coding:utf-8-*- # _author_=zh import os import configparser PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) class Configuration(object): def __init__(self): self.config = configparser.ConfigParser() self.name = PATH+os.sep+"conf"+os.sep+"system.ini" def init_config(self): # 初始化配置文件,IP:服务端IP,port:服务端端口,limit_size:家目录容量,HOME_PATH:用户家目录地址 if not os.path.exists(self.name): self.config["config"] = {"ip": "localhost", "port": 1234} self.config.write(open(self.name, "w", encoding="utf-8", )) def get_config(self, head="config"): ''' 获取配置文件数据 :param head: 配置文件的section,默认取初始化文件config的数据 :return:返回head中的所有数据(列表) ''' self.init_config() # 取文件数据之前生成配置文件 self.config.read(self.name, encoding="utf-8") if self.config.has_section(head): section = self.config.sections() return self.config.items(section[0])
core:
#!/usr/bin/env python # _*_coding:utf-8_*_ # _author_=zh import os import locale import codecs import subprocess PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+os.sep+"home" def order(cmd): ''' 执行命令结果输出到屏幕 :param cmd: 输入的命令 :return: ''' word = subprocess.Popen(args=cmd, cwd=PATH, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) # 将结果decode输出,自动获取不同操作系统的默认编码 return word.stderr.read().decode(codecs.lookup(locale.getpreferredencoding()).name), word.stdout.read().decode(codecs.lookup(locale.getpreferredencoding()).name)
#!/usr/bin/env python # -*-coding:utf-8-*- # Author:zh import socketserver import json import hashlib import os from conf import config import db import core PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) class MyServer(socketserver.BaseRequestHandler): def handle(self): while True: try: data = self.request.recv(1024) if len(data) > 0: cmd_dict = json.loads(data.decode()) action = cmd_dict['action'] if hasattr(self, action): func = getattr(self, action) func(cmd_dict) else: break except (ConnectionResetError, OSError) as e: print(e) break def dir(self, *args): # 查看目录 cmd_dict = args[0] command_data = cmd_dict["command"] command = command_data.split() user_name = cmd_dict["user_name"] home_path = PATH + os.sep + "home" + os.sep + user_name if len(command) > 1: path_data = command[1] if path_data[:len(home_path)] == home_path: answer = core.cmd.order(command_data) else: answer = '路径错误' else: answer = core.cmd.order('%s %s' % (command[0],home_path)) self.request.send(json.dumps(answer).encode()) def cd(self, *args): # 切换目录,默认显示家目录,格式为cd ./path cmd_dict = args[0] dir_path = cmd_dict["dir_path"] now_path = cmd_dict["now_path"] if dir_path[0] is '.': dir_path = dir_path.replace('.', PATH+os.sep+"home"+os.sep+now_path) print("dir_path:", dir_path) if os.path.exists(dir_path): sign = '803' else: sign = '804' else: sign = '804' self.request.send(sign.encode()) def put(self, *args): # 上传 cmd_dict = args[0] filename = cmd_dict["filename"][cmd_dict["filename"].rfind("\")+1:] file_size = cmd_dict["size"] user_name = cmd_dict["user_name"] value = db.data.read(user_name) limit_size = int(value["limit_size"]) home_path = PATH+os.sep+"home"+os.sep+user_name if limit_size > file_size: if os.path.isfile(home_path+os.sep+filename): file_path = home_path+os.sep+filename+'.new' else: file_path = home_path + os.sep + filename sign = "805" else: sign = "806" self.request.send(sign.encode()) if sign == "805": file = open(file_path, 'wb') recv_size = 0 hash_md5 = hashlib.md5() while recv_size < file_size: data = self.request.recv(1024) hash_md5.update(data) file.write(data) recv_size += len(data) else: self.request.send("807".encode()) file.close() hash_pwd = (self.request.recv(1024)).decode() if hash_pwd == hash_md5.hexdigest(): sign = '808' value["limit_size"] = limit_size - file_size db.data.write(user_name, value) else: sign = '809' os.remove(file_path) self.request.send(sign.encode()) def get(self, *args): # 下载 cmd_dict = args[0] file_path = cmd_dict["file_path"] user_name = cmd_dict["user_name"] user_path = PATH+os.sep+"home"+os.sep+user_name if file_path[:len(user_path)] == user_path: if os.path.isfile(file_path): sign = '810' else: sign = '812' else: sign = '811' self.request.send(sign.encode()) if sign == '810': self.request.recv(1024) server_msg = { "length": os.stat(file_path).st_size, "file_name": file_path[file_path.rfind("\")+1:] } self.request.send(json.dumps(server_msg).encode()) exists_length = (self.request.recv(1024)).decode() file = open(file_path, "rb") file.seek(int(exists_length)) hash_md5 = hashlib.md5() for line in file: self.request.send(line) hash_md5.update(line) else: file.close() pwd_md5 = hash_md5.hexdigest() self.request.recv(1024) self.request.send(pwd_md5.encode()) def authenticate(self, *args): # 登陆认证 cmd_dict = args[0] name = cmd_dict["username"] value = db.data.read(name) # 用户存在返回用户数据,不存在返回None if value: num_sign = "801" else: num_sign = "802" send_data = { "sign": num_sign, "value": value } self.request.send(json.dumps(send_data).encode()) # 用户名不存在时注册,需接收注册信息并写入数据库 if not value: user_data = self.request.recv(1024) if len(user_data) > 0: user_data = json.loads(user_data.decode()) db.data.write(user_data["name"], user_data) os.popen("mkdir %s" % (PATH+os.sep+"home"+os.sep+name)) def run(): conf = config.Configuration() conf_data = conf.get_config() servers = socketserver.ThreadingTCPServer((conf_data[0][1], int(conf_data[1][1])), MyServer) servers.serve_forever()
db:
#!/usr/bin/env python # -*-coding:utf-8-*- # _author_=zh import shelve import os PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+os.sep+"db"+os.sep def write(name, data): ''' 存储个人信息 :param data: 存储的数据 :param name: shelve的key ''' file = shelve.open("%sdata" % PATH) file[name] = data file.close() def read(name): ''' 读取个人信息 :param name:存储时传入的name :return:返回个人信息 ''' file = shelve.open("%sdata" % PATH) if name in list(file.keys()): data = file[name] else: data = None file.close() return data
: