github源码:https://github.com/xieyousheng/ftp
思路分析:
作者: xieyousheng 版本:Ftp_v1 开发环境: python3.6.4 程序介绍: 1. 用户认证 2. 多用户同时登陆 3. 每个用户有自己的家目录且只能访问自己的家目录 4. 对用户进行磁盘配额、不同用户配额可不同 5. 用户可以登陆server后,可切换目录 6. 查看当前目录下文件 7. 上传下载文件 8. 传输过程中现实进度条 9. 支持断点续传 10.可通过root对用户操作 使用说明: 1.可以在Linux和Windows都可以运行 2.root用户可以调用所有命令 3.其他用户只能调用了cd,ls,mkdir,rm,wget,put,命令 服务端启动命令 python ftp_server.py start 客户端启动命令 python ftp_client -s 服务端地址 -P 服务端端口 -u 用户名 -p 密码 put 上传 wget 下载 mkdir 创建目录 ls 查看文件信息 rm 删除文件或目录 cd 切换目录 useradd 添加用户 usermod 修改用户 userdel 删除用户 服务端与客户端的功能对应,通过自省/反射来映射功能(hasattr、getattr)
文件目录结构
#coding=utf8 import optparse from socket import * import json import os,sys import struct 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", 258 : "md5 verification", 259 : "Insufficient space left", 800 : "the file exist ,but not enough , is continue?", 801 : "the file exist!", 802 : "ready to receive datas", 803 : "User already exists", 804 : "This directory is used by other users", 805 : "This user does not exist", 806: "Delete home directory or not", 900 : "md5 valdate success", 901: "OK", 902: "the directory exist!" } class ClientHandler(): def __init__(self): ''' 初始化,通过make_connection()得到一个socket连接, 然后执行handler通信处理函数 ''' self.op = optparse.OptionParser() self.op.add_option('-s','--server',dest="server",help="server name or server ip") self.op.add_option('-P', '--port', dest="port", help="server port(0-65535)") self.op.add_option('-u', '--username', dest="username", help="username") self.op.add_option('-p', '--password', dest="password", help="password") self.options,self.argv = self.op.parse_args() self.main_path = os.path.dirname(os.path.abspath(__file__)) self.verify_argv(self.options,self.argv) self.make_connection() self.handler() def handler(self): ''' 如果认证通过就进入通信循环, 接受用户输入的命令,如果是quit或者exit就退出,输入命令为空就跳过此次循环 根据用户输入的命令通过hasattr与getattr进行分发功能 如果没有通过,接关闭socket连接 :return: ''' if self.authenticate(): while True: cmd_info = input(self.pwd).strip() if cmd_info == 'quit' or cmd_info == 'exit':exit() if not cmd_info: continue cmd_list = cmd_info.split() if hasattr(self,cmd_list[0]): func = getattr(self,cmd_list[0]) func(*cmd_list) else: print("无效的命令") else: self.sock.close() def authenticate(self): ''' 认证判断用户输入的username和password是否为None 如果是就提示用户输入,然后进入认证get_auth_result 如果不是为None就直接认证get_auth_result :return: ''' if (self.options.username is None) or (self.options.password is None): username = input("username :") password = input("password :") return self.get_auth_result(username,password) return self.get_auth_result(self.options.username,self.options.password) def get_auth_result(self,username,password): ''' 认证函数 准备一个字典,字典中的action 是固定的 "auth"对应服务端的auth功能,认证功能字典中应该带有用户名和密码 把字典传给resphonse功能进行发送给服务端 通过request功能进行接受服务端发过来的字典 判断服务端发来的状态码 如果为254 即 254 : "Passed authentication" 认证通过, 就把self.user = username self.pwd = 服务端发送过来的路径 如果不为254,直接输入状态码信息 :param username: :param password: :return: ''' data = { "action": "auth", "username":username, "password":password } self.resphonse(data) res = self.request() if res['status_code'] == 254: self.user = username print(STATUS_CODE[res['status_code']]) self.pwd = res["bash"] return True else: print(STATUS_CODE[res['status_code']]) def request(self): ''' 接受功能函数, 从服务端接受包的长度,然后再从服务端接受包,这样可以解决粘包的问题,这里包编码前的格式为json 接收到包之后进行解码,然后把json字符串转为为原有的格式(字典) :return: ''' length = struct.unpack('i',self.sock.recv(4))[0] data = json.loads(self.sock.recv(length).decode('utf-8')) return data def resphonse(self,data): ''' 发送功能 把接受到的字典,转换为json字符串然后进行编码 使用struct.pack封装json字符串的长度 向服务端发送长度,然后再发送已经编码的json字符串 :param data: :return: ''' data = json.dumps(data).encode('utf8') length = struct.pack('i',len(data)) self.sock.send(length) self.sock.send(data) def make_connection(self): ''' 创建连接 :return: ''' self.sock = socket(AF_INET,SOCK_STREAM) self.sock.connect((self.options.server,int(self.options.port))) def verify_argv(self,options,argv): ''' 端口参数验证 :param options: :param argv: :return: ''' if int(options.port) > 0 and int(options.port) < 65535: return True else: exit("端口范围0-65535") def processbar(self,num,total): # 进度条 rate = num / total rate_num = int(rate * 100) is_ok = 0 if rate_num == 100: r = ' %s>%d%% ' % ('=' * rate_num, rate_num,) is_ok = 1 else: r = ' %s>%d%%' % ('=' * rate_num, rate_num,) sys.stdout.write(r) sys.stdout.flush return is_ok def put(self,*cmd_list): cmd_list = cmd_list[1:] if not cmd_list: print("请输入要上传的文件路径!") return file_path = os.path.join(self.main_path,cmd_list[0]) filename = os.path.basename(cmd_list[0]) filesize = os.path.getsize(file_path) data = { "action" : "put", "filename" : filename, "filesize" :filesize } if len(cmd_list) == 1: data['target_path']= "." else: data['target_path'] = cmd_list[1] self.resphonse(data) is_exist = self.request() f = open(file_path,'rb') if is_exist["status_code"] == 802: has_received = 0 f.seek(has_received) elif is_exist["status_code"] == 801 or is_exist["status_code"] == 259: print(STATUS_CODE[is_exist["status_code"]]) return elif is_exist["status_code"] == 800: u_choice = input("the file exist,but not enough,is continue?[Y/N]").strip() self.resphonse({"choice": u_choice.upper()[0]}) if u_choice.upper()[0] == "Y": has_received = self.request()['has_received'] f.seek(has_received) else: has_received = 0 f.seek(has_received) while has_received < filesize: file_data = f.read(1024) self.sock.send(file_data) has_received += len(file_data) self.processbar(has_received,filesize) f.close() print("put success!") def mkdir(self,*cmd_list): data = { "action" : "mkdir", "dirname": cmd_list[1:] } self.resphonse(data) res = self.request() if res["status_code"] != 901: print(STATUS_CODE[res["status_code"]]) def rm(self,*cmd_list): data = { "action":"rm", "dirname":cmd_list[1:] } self.resphonse(data) res = self.request() def cd(self,*cmd_list): if len(cmd_list)==1 : return data = { "action" : "cd", "dirname" : cmd_list[1] } self.resphonse(data) res = self.request() self.pwd = res["bash"] def ls(self,*cmd_list): data = { "action": "ls", } if len(cmd_list) == 1: data["dirname"] = "." else: data["dirname"] = cmd_list[1] self.resphonse(data) res = self.request() if res["status_code"] == 903 : print(res["data"]) else: print(' '.join(res["data"])) def useradd(self,*cmd_list): if self.user != 'root': print("你无权限执行此命令!") return data = { "action" : "useradd", } data = self.useradd_verify_argv(*cmd_list,**data) self.resphonse(data) print(STATUS_CODE[self.request()["status_code"]]) def useradd_verify_argv(self,*cmd_list,**data): op = optparse.OptionParser() op.add_option('-u', '--username', dest="username") op.add_option('-p', '--password', dest="password") op.add_option('-d', '--drictory', dest="drictory") op.add_option('-m', '--maxsize', dest="maxsize") options, argv = op.parse_args(list(cmd_list)) data["username"] = options.username data["password"] = options.password data["home"] = options.drictory data["homemaxsize"] = options.maxsize if data["username"] is None: data["username"] = input("username : ") if data["action"] == "useradd": if data["password"] is None: data["password"] = input("password : ") if data["home"] is None: data["home"] = data["username"] if data["homemaxsize"] is None: data["homemaxsize"] = 1000 return data def usermod(self,*cmd_list): if self.user != 'root': print("你无权限执行此命令!") return data = { "action" : "usermod", } data = self.useradd_verify_argv(*cmd_list, **data) self.resphonse(data) print(STATUS_CODE[self.request()["status_code"]]) def userdel(self,*cmd_list): if self.user != 'root': print("你无权限执行此命令!") return data = { "action":"userdel", "username":cmd_list[1] } self.resphonse(data) res = self.request() if res["status_code"] == 805: print(STATUS_CODE[res["status_code"]]) return choice = input("Delete home directory or not,Y/N:").strip() self.sock.send(choice.upper()[0].encode('utf8')) print(STATUS_CODE[res["status_code"]]) def wget(self,*cmd_list): data = { "action" : "wget", } file_path = os.path.dirname(os.path.abspath(__file__)) if len(cmd_list) == 1: print("请输入文件名!") return elif len(cmd_list) >= 3: if os.path.isabs(cmd_list[2]): file_path = cmd_list[2] if not os.path.exists(cmd_list[2]): print("目标路径不存在!") return else: file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),cmd_list[2]) if not os.path.exists(file_path): print("目标路径不存在!") return data["filename"] = cmd_list[1] self.resphonse(data) res = self.request() if res["status_code"] == 256: print(STATUS_CODE[res["status_code"]]) return try: f = open(os.path.join(file_path,os.path.basename(data["filename"])),'wb') except PermissionError as e: print(e) self.sock.send('0'.encode('utf-8')) return if res["filesize"] == 0 : f.close() return self.sock.send('1'.encode('utf-8')) size = 0 while True: file_data = self.sock.recv(4096) f.write(file_data) size += len(file_data) if self.processbar(size,res["filesize"]): break f.close() if __name__ == '__main__': c = ClientHandler() c.handler()
import os,sys BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASEDIR) from src import main if __name__ == '__main__': main.ArgvHandler()
[root] username = root password = 123 home = root homemaxsize = 1000 [xie] username = xie password = 123 home = xie homemaxsize = 1000
import os BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) IP = '0.0.0.0' PORT = 8090 HOME_PATH = os.path.join(BASEDIR,'data') ACCOUNT_PATH = os.path.join(BASEDIR,'conf','account.cfg') 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", 259 : "Insufficient space left", 800 : "the file exist ,but not enough , is continue?", 801 : "the file exist!", 802 : "ready to receive datas", 803 : "User already exists", 804 : "This directory is used by other users", 805 : "This user does not exist", 900 : "md5 valdate success", 901 : "OK", 902 : "the directory exist!", 903 : "the directory not exist!", 904 : "No such file or directory" }
import os from conf import setting ''' 服务端处理客户的类 ''' class Client: def __init__(self,user,passwd,home,maxsize): ''' :param user: 用户名 :param passwd: 密码 :param home: 家目录 :param maxsize: 家目录空间大小 self.basedir ftp的根目录,所有家目录都是从这个目录开始 self.path 目录列表,最开始的是家目录,然后添加到列表的是cd的目录,这里就限制了用户的根目录就是自己的家目录 self.free 表示用户家目录剩余的空间大小 ''' self.user = user self.passwd = passwd self.basedir = setting.HOME_PATH self.home = home self.path = [self.home] self.maxsize = maxsize self.free = float(maxsize) - float(self.use_size()/1024) def get_bash(self): return '%s @ %s >>> :' % (self.user,self.path[-1]) def use_size(self): ''' 用户家目录已使用的大小 :param path: :return: ''' size = 0 for i in os.listdir(os.path.join(self.basedir,self.home)): if os.path.isdir(i): size += self.use_size(os.path.join(self.basedir,self.home,i)) size += os.path.getsize(os.path.join(self.basedir,self.home,i)) size = float(size/1024) #由于系统4K 对齐,所以最小存储单位为 4KB res = divmod(size,4) if res[1]: res = res[0] + 1 else: res = res[0] return res def get_path(self): ''' 获取当前所在目录的绝对路径 :return: ''' basepath = self.basedir for i in self.path: basepath = os.path.join(basepath,i) return basepath if __name__ == '__main__': c = Client('xie','asd','src',100) c.free_size(c.home)
import optparse from src import server_handler import socketserver from conf.setting import * class ArgvHandler(): ''' 参数处理类, 使用optparse.OptionParser来处理参数 ''' def __init__(self): self.op = optparse.OptionParser() options,argv = self.op.parse_args() self.verify_argv(options,argv) def verify_argv(self,options,argv): ''' 验证参数函数,通过获取的argv 参数,然后利用hasattr和getattr反射功能来分发功能,比如 start 、help等 :param options: :param argv: :return: ''' cmd = argv[0] if hasattr(self,cmd): func = getattr(self,cmd) func() else: print("参数错误!") def start(self): ''' 服务启动功能 :return: ''' print("server is working...") s = socketserver.ThreadingTCPServer((IP,PORT), server_handler.FtpHandler) s.serve_forever() def help(self): pass
#coding=utf8 import socketserver import struct import json,os import configparser from conf import setting from lib import client class FtpHandler(socketserver.BaseRequestHandler): ''' sockerserver多线程类 ''' def ser_recv(self): ''' 接受客户端数据 :return: 返回客户端发给来的字典 ''' try: length = struct.unpack('i',self.request.recv(4))[0] return json.loads(self.request.recv(length).decode('utf8')) except Exception as e: print(e) return False def ser_resphone(self,data): ''' 给客户端相应的函数 :param data: :return: ''' data = json.dumps(data).encode('utf-8') length = struct.pack('i',len(data)) self.request.send(length) self.request.sendall(data) def handle(self): ''' 通信循环,获取客户端发过来的字典,通过字典中的action来判断功能,用hasattr/getattr来分发功能 :return: ''' while True: data = self.ser_recv() if not data: break if data.get("action") is not None: if hasattr(self,data.get("action")): func = getattr(self,data["action"]) func(**data) else: print("无效的操作") else: print("无效的操作!") def auth(self,**data): username = data["username"] password = data["password"] user = self.authenticate(username,password) if user: res = { "status_code": 254, 'bash': self.user.get_bash() } else: res = { "status_code":253, } self.ser_resphone(res) def authenticate(self,username,password): cfg = configparser.ConfigParser() cfg.read(setting.ACCOUNT_PATH) if username in cfg.sections() and password == cfg[username]["password"]: self.user = client.Client(username,password,cfg[username]['home'],cfg[username]['homemaxsize']) return username def cd(self,**data): if data["dirname"] == ".": res = {"bash":self.user.get_bash()} elif data["dirname"] == "..": if len(self.user.path) > 1: self.user.path.pop() os.chdir(self.user.get_path()) res = {"bash":self.user.get_bash()} else: self.user.path.append(data["dirname"]) os.chdir(self.user.get_path()) res = {"bash":self.user.get_bash()} self.ser_resphone(res) def ls(self,**data): pwd = self.user.get_path() if data["dirname"] == "." : new_path = pwd else: new_path = os.path.join(pwd, data["dirname"]) if (data["dirname"] == ".") or (os.path.exists(new_path)): listdir = os.listdir(new_path) res = { "status_code":901, "data" : listdir } else: res = { "status_code": 903, "data": "该目录不存在" } self.ser_resphone(res) def wget(self,**data): file_name = os.path.basename(data["filename"]) file_path = os.path.join(self.user.get_path(),data["filename"]) if os.path.exists(file_path): if os.path.isfile(file_path): self.ser_resphone({"status_code": 901,"filesize":os.path.getsize(file_path)}) if self.request.recv(1).decode("utf8") == "1": with open(file_path,'rb') as f: while True: data = f.read(4096) if not data: break self.request.send(data) return return self.ser_resphone({"status_code":256}) def put(self,**data): file_name = data['filename'] file_size = data['filesize'] if self.user.free < file_size/1024/1024: return self.ser_resphone({"status_code": 259}) target_path = data['target_path'] if target_path == '.': abs_path = os.path.join(self.user.get_path(),file_name) else: abs_path = os.path.join(self.user.get_path(),target_path,file_name) has_received = 0 if os.path.exists(abs_path): file_has_size = os.path.getsize(abs_path) if file_has_size < file_size: #断点续传 self.ser_resphone({"status_code":800}) client_choice = self.ser_recv() if client_choice["choice"] == "Y": self.ser_resphone({"has_received":file_has_size}) f = open(abs_path,"ab") has_received += file_has_size else: f = open(abs_path,"wb") else: return self.ser_resphone({"status_code":801}) else: self.ser_resphone({"status_code":802}) f = open(abs_path,"wb") while has_received < file_size: data = self.request.recv(1024) f.write(data) has_received += len(data) f.close() def mkdir(self,**data): dir_list = data["dirname"] for i in dir_list: try: os.makedirs(os.path.join(self.user.get_path(),i)) except FileExistsError as e: print(e) res = {"status_code":902} return self.ser_resphone(res) res = {"status_code":901} self.ser_resphone(res) def rm_handle(self,**data): recv_data = data basedir = self.user.get_path() if data["action"] == "userdel": basedir = setting.HOME_PATH for i in recv_data["dirname"]: path = os.path.join(basedir,*data["path"],i) print(path) if os.path.exists(path): if os.path.isfile(path): os.remove(path) else: data["dirname"] =[i for i in os.listdir(path)] data["path"].append(i) self.rm_handle(**data) data["path"].pop() os.rmdir(path) else: return {"status": 904 ,"dirname":i} return {"status": 901} def rm(self,**data): data["path"] = [] res = self.rm_handle(**data) self.ser_resphone(res) def useradd(self,**data): config = configparser.ConfigParser() code = self.useradd_verify_argv(config,**data) if code == 803 or code == 804:return self.ser_resphone({"status_code":code}) del data["action"] config[data["username"]] = data if not os.path.exists(os.path.join(setting.HOME_PATH,data['home'])): os.mkdir(os.path.join(setting.HOME_PATH,data['home'])) with open(setting.ACCOUNT_PATH, 'w') as configfile: config.write(configfile) self.ser_resphone({"status_code": code}) def useradd_verify_argv(self,conf,**data): conf.read(setting.ACCOUNT_PATH) if data["action"] == "useradd": if data["username"] in conf.sections(): return 803 for i in conf.sections(): if data["home"] == conf[i]["home"]: return 804 return 901 elif data["action"] == "usermod": if data["username"] in conf.sections(): if data["password"] is None: data["password"] = conf[data["username"]]["password"] print(data["home"],conf[data["username"]]["home"]) if data["home"] != conf[data["username"]]["home"]: for i in conf.sections(): if data["home"] == conf[i]["home"]: return 804 del data["action"] return 901, data else: return 805 else: if data["username"] in conf.sections(): return 901 else: return 805 def usermod(self,**data): config = configparser.ConfigParser() code = self.useradd_verify_argv(config, **data) if code == 805 or code == 804 : return self.ser_resphone({"status_code": code}) if not os.path.exists(os.path.join(setting.HOME_PATH,data['home'])): os.mkdir(os.path.join(setting.HOME_PATH, data['home'])) config[data["username"]] = code[1] with open(setting.ACCOUNT_PATH, 'w') as configfile: config.write(configfile) self.ser_resphone({"status_code": code[0]}) def userdel(self,**data): config = configparser.ConfigParser() config.read(setting.ACCOUNT_PATH) code = self.useradd_verify_argv(config, **data) if code == 805 : return self.ser_resphone({"status_code": code}) self.ser_resphone({"status_code": code}) choice = self.request.recv(1).decode('utf8') if choice == "Y": home = config[data["username"]]["home"] self.rm_handle(**{"dirname":[home],"action":"userdel","path":[]}) config.remove_section(data["username"]) config.write(open(setting.ACCOUNT_PATH, 'w'))
服务端启动:
客户端启动:
ls命令:
put命令
断点续传,将s1.avi 上传到 a目录
上面用ctrl+c中断了上传
cd命令:
wget命令:
mkdir命令
rm命令
useradd命令
usermod命令
userdel命令
断点续传