项目介绍:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
实现的原理:
服务器端启用端口监听,并对每一连接启用一个线程,对用户登陆密码采用SHA512进行加密并进行匹配,当用户登陆成功后,实例化FTPS,并引导客户端进入主命令模式,
然后实现FTP的上传功能、下载功能、新建目录、删除文件或目录、切换目录等实例化操作,同时对相关上传下载进行进度条显示,服务器端显示下载或上传文件的大小等
客户端与服务器协商建立连接后,进行用户身份登陆,登陆成功接收服务器指令,转入命令输入窗口,同时对put 与 get命令进行判断,实现特定的上传与下载功能
核心代码实现如下:
服务器端
main.py
#!/usr/bin/env python3.5 # -*-coding:utf8-*- import os,sys,socket,pickle BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASEDIR) from conf import setting from core import file_handler from core import db_handler import select,hashlib import threading def login(username,password): """ FTP登陆验证函数 :param username: :param password: :return: # testDict ={"username":"jjb","password":"123456","file_dir":"E:python","file_size":500} # file = 'jjb.pkl' # fp = open(file,'wb') # pickle.dump(testDict,fp) # fp.close() f = open("jjb.pkl","rb") data = pickle.loads(f.read()) f.close() print(data) """ #实例化加密函数 hash = hashlib.sha512() db= db_handler.handler(setting.DATABASE,username) if os.path.isfile(db): f = open(db,"rb") data = pickle.loads(f.read()) f.close() if username == data["name"]: hash.update(bytes(data["password"],"utf8")) hash_pwd = hash.hexdigest() if hash_pwd == password: filedir = data["file_dir"] filesize = data["file_size"] return "True|%s|%s"%(filedir,filesize) else: return "False||" else: return "False||" else: return "False||" def process(conn,addr): flage = "False" # 接收客户端连接请求信息 info = conn.recv(1000) if info.decode() == "connect": conn.send(bytes("login","utf8")) # 接收用户及密码信息 while flage =="False": user_check =conn.recv(8000) # 分割用户名及密码 username,password = str(user_check.decode()).split("|") # 调用登陆验证函数 login_ack = login(username,password) flage,home,size = str(login_ack).split("|") # print(flage,home,size) # print("user_input:",username,"user_pass:",password) if flage =="True": # 登陆成功发送登陆确认信息给客户端 conn.send(bytes("login_ack","utf8")) # 实例化FTPserver ftp = file_handler.FTPs(username,conn,home,size) # 登陆用户,数据连接,工作目录,磁盘配额 ftp.run() break else: # 登陆失败,发送给客户端重新验证 conn.send(bytes("登陆失败!","utf8")) def ftp_server(): ''' 启动FTP服务器端,开启线程监听 :return: ''' server = socket.socket() server.bind((setting.IP_PORT["host"],setting.IP_PORT["port"])) server.listen(10) while True: r,w,e = select.select([server,], [], [], 1) for i,server in enumerate(r): conn,addr = server.accept() # 创建线程 t = threading.Thread(target=process, args=(conn, addr)) # 启动线程 t.start() server.close() def run(): ftp_server() if __name__ =="__main__": run()
file_handler.py
#!/usr/bin/env python3.5 # -*-coding:utf8-*- import os,sys BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASEDIR) import re from core import db_handler from conf import setting import pickle class FTPs(object): ''' ftp操作命令方法: ''' def __init__(self,username,conn,home,total_size): ''' 初始化参数 :param username: 操作用户名 :param conn: sock连接 :param home: 用户根目录 :param total_size: 磁盘配额 :return: ''' self.username = username self.conn = conn self.root = home self.home = self.root self.total_size = int(total_size) self.cmd_file = None # 文件指令 self.psize = 4096 # 文件分片 def getdirsize(self,space): ''' 计算磁盘空间大小 :return: ''' self.dirsize = 0 for root,dirs,files in os.walk(space): self.dirsize += (sum([os.path.getsize(os.path.join(root,name))for name in files])/1024) return int(self.dirsize) def put(self): ''' 上传文件 :return: ''' if self.cmd_file: self.user_space = int(self.getdirsize(self.root)/1024) # 组合接收字符串 self.file_root = '%s\%s'% (self.home,self.cmd_file) # # 获取文件名 self.f =os.path.basename(self.file_root) if os.path.isdir(self.home): os.chdir(self.home) else: os.makedirs(self.home) os.chdir(self.home) try: self.conn.send(bytes("f_ack","utf8")) self.size = str(self.conn.recv(1024).decode()).split("|") if self.size[0]== "fsize": self.fss = int(self.size[1]) self.f_total_size = int(self.user_space + (self.fss/1024/1024)) if self.f_total_size < self.total_size: # 判断空间是否超额 self.conn.send(bytes("f_ack_ready","utf8")) self.bsize = 0 print("需要上传文件大小:",self.fss) # 打开文件 f=open(self.f,'wb') while self.bsize < self.fss: data = self.conn.recv(self.psize) self.bsize += len(data) f.write(data) self.conn.send(bytes("ok","utf8")) print("实际已上传文件大小:",self.bsize) else: self.conn.send(bytes("上传空间不足!无法上传,你当前磁盘配额为%sM"%self.total_size,"utf8")) except Exception as ex: self.conn.send(bytes(ex,"utf8")) else: self.conn.send(bytes("请上传文件,文件不能为空","utf8")) def get(self): ''' 下载文件 :return: ''' if self.cmd_file: os.chdir(self.home) # 进入用户根目录 self.file = os.getcwd()+"\"+ self.cmd_file if os.path.isfile(self.file): f = open(self.file, 'rb') self.fsize = os.path.getsize(self.file) # 获取要发送文件的大小 self.conn.send(bytes("f_ack_read","utf8")) self.conn.recv(1000) print("需发送文件大小:",self.fsize) self.conn.send(bytes("fsize|%s"%self.fsize,"utf8")) # 发送文件大小及要发送准备完毕指令 if self.conn.recv(1000).decode() == "f_ack": # 接收对方是否准备就绪 self.fsize = int(self.fsize) self.size = 0 ack ="" while self.size < self.fsize and ack !="ok": data = f.read(self.fsize) # 一次读取分片大小4096 self.conn.send(data) self.size += len(data) print("实际发送文件大小:",self.size) ack = self.conn.recv(1000).decode() # 接收客户端是否下载完指令 self.conn.send(bytes("成功","utf8")) else: self.conn.send(bytes("接收失败","utf8")) else: self.conn.send(bytes("文件不存在","utf8")) else: self.conn.send(bytes("请输入文件名","utf8")) def dir(self): ''' 查看文件 :return: ''' self.current_space =int(self.getdirsize(self.home)) # 文件列表 self.li = "" # 目录列表 self.dl = "" try: os.chdir(self.home) except: os.makedirs(self.home) os.chdir(self.home) try: if os.listdir(os.getcwd()): for self.i in os.listdir(os.getcwd()): self.file = os.getcwd()+'\'+self.i if os.path.isfile(self.file): # 获取文件大小 self.fsize = int(os.path.getsize(self.file)/1024) if self.fsize < 1: self.fsize = 4 else: self.fsize +=4 self.li += '%s -rw-rw-rw- 占用大小:%skb '% (self.i,self.fsize) else: self.dl += '%s '%self.i self.conn.send(bytes("目录: %s 文件: %s 当前目录空间大小:%skb"%(self.dl,self.li,self.current_space),"utf8")) else: self.conn.send(bytes("当前目录为:%s"%(self.home),"utf8")) except Exception as ex: self.conn.send(bytes(ex,"utf8")) def cd(self): ''' 进入目录 :return: ''' if self.cmd_file: os.chdir(self.home) # 先进入到工作目录 self.dir_change = os.path.abspath(os.path.join(self.home,"%s\%s"%(self.home,self.cmd_file))) if self.root in self.dir_change: try: os.chdir(self.dir_change) self.home = self.dir_change self.conn.send(bytes("当前工作目录为:%s"%self.home,"utf8")) except: os.makedirs(self.dir_change) os.chdir(self.dir_change) self.home = self.dir_change self.conn.send(bytes("当前工作目录为:%s"%self.home,"utf8")) else: self.conn.send(bytes("当前工作目录为:%s 更改失败!"%self.home,"utf8")) else: os.chdir(self.home) self.conn.send(bytes("当前工作目录为:%s"%self.home,"utf8")) def mkd(self): ''' 创建目录 :return: ''' if self.cmd_file: try: os.makedirs(self.cmd_file) self.conn.send(bytes("创建目录成功!","utf8")) except Exception as ex: self.conn.send(bytes("创建目录失败!原因:%s"%ex,"utf8")) else: self.conn.send(bytes("请输入文件夹名!","utf8")) def delete(self): ''' 删除文件 :return: ''' os.chdir(self.home) # 进入用户根目录 try: self.file = self.home+'\'+ self.cmd_file if os.path.isfile(self.file): os.remove(self.cmd_file) self.conn.send(bytes("文件:%s删除成功!"%self.cmd_file,"utf8")) else: os.removedirs(self.cmd_file) self.conn.send(bytes("目录删除成功!","utf8")) os.chdir(self.root) except Exception: if os.path.isdir(self.root): self.conn.send(bytes("删除失败!","utf8")) else: os.makedirs(self.root) self.conn.send(bytes("删除失败!","utf8")) def help(self): ''' FTP帮助信息 :return: ''' self.conn.send(bytes(""" FTP服务器操作方法有: put------>上传文件至服务器 get------>从服务器上下载文件 dir------>查看服务器文件列表 cd------->进入指定文件夹 delete--->删除文件 mkd ----->创建目录 help----->帮助信息 q ------->退出 ""","utf8")) def run(self): while True: # try: # # 接收客户端发来的命令信息 self.cmd = self.conn.recv(1000) self.cmd_action = str(self.cmd.decode()) # 判断命令是否含有空格 self.fg = re.search("s","%s"%self.cmd_action) if self.fg: self.cmd_action,self.cmd_file = str(self.cmd_action).split(" ") else: self.cmd_file =None # print("cmd_action:",self.cmd_action,"cmd_file:",self.cmd_file) if hasattr(FTPs,self.cmd_action): func = getattr(self,self.cmd_action) func() continue else: self.conn.send(b'command is not found!') continue # except Exception as ex: # print("系统异常:%s"%ex)
客户端
client.py
#!/usr/bin/env python3.5 # -*-coding:utf8-*- import sys,os,re import socket,hashlib BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASEDIR) from core import file_handler from conf import setting def login(): hash = hashlib.sha512() while True: user_input = input("请输入用户名:").strip() pass_input = input("请输入密码:").strip() if len(user_input) !=0 and len(pass_input) != 0: hash.update(bytes(pass_input,"utf8")) sha_pwd = hash.hexdigest() user = "%s|%s"% (user_input,sha_pwd) return user break def ftp_client(): sk = socket.socket() sk.connect((setting.IP_PORT["host"],setting.IP_PORT["port"])) while True: flage = False sk.send(bytes("connect","utf8")) msg = sk.recv(100) print("欢迎访问FTP服务器,请根据提示进行操作") if msg.decode() == "login": while flage == False: login_user =login() username,password = str(login_user).split("|") sk.send(bytes(login_user,"utf8")) user_info = sk.recv(1000) if user_info.decode() == "login_ack": print("登陆成功!") flage = True break print(user_info.decode()) while flage: cmd_action = input("请输入操作命令如:get fy.py or help :").strip() if len(cmd_action) == 0:continue if cmd_action == "q": sys.exit() # 判断命令是否含有空格 fg = re.search("s","%s"%cmd_action) if fg: cmd,cmd_file = str(cmd_action).split(" ") ftp = file_handler.ftpc(sk,username,cmd_action,setting.DATABASE["local"]) if hasattr(ftp,cmd): func = getattr(ftp,cmd) func() continue else: cmd_file =None sk.send(bytes(cmd_action,"utf8")) rec_msg = sk.recv(8000) print(rec_msg.decode()) if flage == "False": sk.send(bytes("connect","utf8")) sk.close() def run(): ftp_client() if __name__ == "__main__": run()
file_handler.py
#!/usr/bin/env python3.5 # -*-coding:utf8-*- import sys,os,re import socket BASEDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASEDIR) class ftpc(object): def __init__(self,sk,username,cmd_action,home): self.sk = sk self.username = username self.cmd_action = cmd_action self.home = home def put(self): ''' 上传文件 :return: ''' try: os.chdir(self.home) except: os.makedirs(self.home) os.chdir(self.home) # 判断命令是否含有空格 fg = re.search("s","%s"%self.cmd_action) if fg: self.cmd,self.cmd_file = str(self.cmd_action).split(" ") if os.path.isfile(os.getcwd()+"\"+self.cmd_file): self.sk.send(bytes(self.cmd_action,"utf8")) # 发送动作命令 rec_msg = self.sk.recv(8000) if rec_msg.decode() == "f_ack": f = open(self.cmd_file, 'rb') self.fsize = os.path.getsize(self.cmd_file) # 获取要发送文件的大小 self.sk.send(bytes("fsize|%s"%self.fsize,"utf8")) # 发送文件大小 self.ack = self.sk.recv(1000) if self.ack.decode() =="f_ack_ready": self.fsize = int(self.fsize) self.size = 0 ack ="" while self.size < self.fsize and ack !="ok": data = f.read(4095) # 一次读取分片大小4095 self.sk.send(data) self.size += len(data) count = int(self.size/self.fsize*100) print('#'*count,"->",(count),"%") ack = self.sk.recv(1000).decode() if ack =="ok": print("上传成功") else: print("上传失败") else: print(self.ack.decode()) else: print("上传文件失败:%s"%rec_msg.decode()) else: print("上传文件失败,请输入正确的文件名!") else: print("上传文件失败,请输入正确的文件名!") def get(self): ''' 下载文件 :return: ''' try: os.chdir(self.home) except: os.makedirs(self.home) os.chdir(self.home) # 判断命令是否含有空格 fg = re.search("s","%s"%self.cmd_action) if fg: self.cmd,self.cmd_file = str(self.cmd_action).split(" ") else: self.cmd_file =None self.sk.send(bytes(self.cmd_action,"utf8")) rec_msg = self.sk.recv(8000) if rec_msg.decode() == "f_ack_read": self.rec = self.sk.send(bytes("ok","utf8")) self.rec_size = self.sk.recv(2048) self.ack_rec= str(self.rec_size.decode()).split("|") self.sk.send(bytes("f_ack","utf8")) self.ack_s =int(self.ack_rec[1]) print(self.ack_s) self.re_s = 0 f = open(self.cmd_file,"wb") while self.re_s < self.ack_s: xx = self.re_s/self.ack_s*100 data = self.sk.recv(4096) self.re_s += len(data) # print(data.decode("gbk")) f.write(data) count = int(xx) print('#'*count,"->",(count+1),"%") self.sk.send(bytes("ok","utf8")) print(self.re_s) self.ack_ok = self.sk.recv(1024) print("接收文件:%s"%self.ack_ok.decode()) else: print("接收文件失败:%s"%rec_msg.decode())
如下是重要模块进行收藏:
OS模块
os.getcwd() 获取当前工作目录,即当前python脚本工作的目录路径 os.chdir("dirname") 改变当前脚本工作目录;相当于shell下cd os.curdir 返回当前目录: ('.') os.pardir 获取当前目录的父目录字符串名:('..') os.makedirs('dirname1/dirname2') 可生成多层递归目录 os.removedirs('dirname1') 若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推 os.mkdir('dirname') 生成单级目录;相当于shell中mkdir dirname os.rmdir('dirname') 删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdir dirname os.listdir('dirname') 列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印 os.remove() 删除一个文件 os.rename("oldname","newname") 重命名文件/目录 os.stat('path/filename') 获取文件/目录信息 os.sep 输出操作系统特定的路径分隔符,win下为"\",Linux下为"/" os.linesep 输出当前平台使用的行终止符,win下为" ",Linux下为" " os.pathsep 输出用于分割文件路径的字符串 os.name 输出字符串指示当前使用平台。win->'nt'; Linux->'posix' os.system("bash command") 运行shell命令,直接显示 os.environ 获取系统环境变量 os.path.abspath(path) 返回path规范化的绝对路径 os.path.split(path) 将path分割成目录和文件名二元组返回 os.path.dirname(path) 返回path的目录。其实就是os.path.split(path)的第一个元素 os.path.basename(path) 返回path最后的文件名。如何path以/或结尾,那么就会返回空值。即os.path.split(path)的第二个元素 os.path.exists(path) 如果path存在,返回True;如果path不存在,返回False os.path.isabs(path) 如果path是绝对路径,返回True os.path.isfile(path) 如果path是一个存在的文件,返回True。否则返回False os.path.isdir(path) 如果path是一个存在的目录,则返回True。否则返回False os.path.join(path1[, path2[, ...]]) 将多个路径组合后返回,第一个绝对路径之前的参数将被忽略 os.path.getatime(path) 返回path所指向的文件或者目录的最后存取时间 os.path.getmtime(path) 返回path所指向的文件或者目录的最后修改时间
sys模块
sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0) sys.version 获取Python解释程序的版本信息 sys.maxint 最大的Int值 sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值 sys.platform 返回操作系统平台名称 sys.stdout.write('please:') val = sys.stdin.readline()[:-1]
re 模块
匹配格式 模式 描述 ^ 匹配字符串的开头 $ 匹配字符串的末尾。 . 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。 [...] 用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k' [^...] 不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。 re* 匹配0个或多个的表达式。 re+ 匹配1个或多个的表达式。 re? 匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式 re{ n} re{ n,} 精确匹配n个前面表达式。 re{ n, m} 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 a| b 匹配a或b (re) G匹配括号内的表达式,也表示一个组 (?imx) 正则表达式包含三种可选标志:i, m, 或 x 。只影响括号中的区域。 (?-imx) 正则表达式关闭 i, m, 或 x 可选标志。只影响括号中的区域。 (?: re) 类似 (...), 但是不表示一个组 (?imx: re) 在括号中使用i, m, 或 x 可选标志 (?-imx: re) 在括号中不使用i, m, 或 x 可选标志 (?#...) 注释. (?= re) 前向肯定界定符。如果所含正则表达式,以 ... 表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。 (?! re) 前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功 (?> re) 匹配的独立模式,省去回溯。 w 匹配字母数字 W 匹配非字母数字 s 匹配任意空白字符,等价于 [ f]. S 匹配任意非空字符 d 匹配任意数字,等价于 [0-9]. D 匹配任意非数字 A 匹配字符串开始 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。c z 匹配字符串结束 G 匹配最后匹配完成的位置。 匹配一个单词边界,也就是指单词和空格间的位置。例如, 'er' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。 B 匹配非单词边界。'erB' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。 , , 等. 匹配一个换行符。匹配一个制表符。等 1...9 匹配第n个分组的子表达式。 10 匹配第n个分组的子表达式,如果它经匹配。否则指的是八进制字符码的表达式。 正则表达式常用5种操作 re.match(pattern, string) # 从头匹配 re.search(pattern, string) # 匹配整个字符串,直到找到一个匹配 re.split() # 将匹配到的格式当做分割点对字符串分割成列表 1 2 >>>m = re.split("[0-9]", "alex1rain2jack3helen rachel8") >>>print(m) 输出: ['alex', 'rain', 'jack', 'helen rachel', ''] re.findall() # 找到所有要匹配的字符并返回列表格式 1 2 >>>m = re.findall("[0-9]", "alex1rain2jack3helen rachel8") >>>print(m)<br> 输出:['1', '2', '3', '8'] re.sub(pattern, repl, string, count,flag) # 替换匹配到的字符 1 2 m=re.sub("[0-9]","|", "alex1rain2jack3helen rachel8",count=2 ) print(m) 输出:alex|rain|jack3helen rachel8 正则表达式实例 字符匹配 实例 描述 python 匹配 "python". 字符类 实例 描述 [Pp]ython 匹配 "Python" 或 "python" rub[ye] 匹配 "ruby" 或 "rube" [aeiou] 匹配中括号内的任意一个字母 [0-9] 匹配任何数字。类似于 [0123456789] [a-z] 匹配任何小写字母 [A-Z] 匹配任何大写字母 [a-zA-Z0-9] 匹配任何字母及数字 [^aeiou] 除了aeiou字母以外的所有字符 [^0-9] 匹配除了数字外的字符 特殊字符类 实例 描述 . 匹配除 " " 之外的任何单个字符。要匹配包括 ' ' 在内的任何字符,请使用象 '[. ]' 的模式。 d 匹配一个数字字符。等价于 [0-9]。 D 匹配一个非数字字符。等价于 [^0-9]。 s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ f v]。 S 匹配任何非空白字符。等价于 [^ f v]。 w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。 W 匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'。 re.match与re.search的区别 re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。