python开发多用户在线FTP
程序员文章站
2022-10-05 08:50:53
import os BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATABASE=os.path.join(BASE_DIR,'database','home',) #用户属主目录 USER_DB=os.p ......
功能实现
作业:开发一个支持多用户在线的ftp程序
要求:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
对用户进行磁盘配额,每个用户的可用空间不同
允许用户在ftp server上随意切换目录
允许用户查看当前目录下文件
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条
附加功能:支持文件的断点续传
服务端
|-------conf(配置文件夹)
| |----settings.py 路径配置信息
|
|-------database(数据库文件夹)
| |----home(家目录)
| |----public
|
|-------modules(服务端功能模块)
| |----server.py 服务端线程交互主模块。。。。。
| |----user.py 用户验证需要的hash 和 login 验证
|
|-------user_db(用户配置项信息)本来想就单文件放,后来觉得还是建了目录
|
|-------start.py 服务端程序启动文件
服务端:
import os base_dir=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) database=os.path.join(base_dir,'database','home',) #用户属主目录 user_db=os.path.join(base_dir,'user_db') #用户账户数据库 public=os.path.join(base_dir,'database','public') #===============下列代码与配置无关 为查询目录大小----------------------- # for root, dirs, files in os.walk(database, topdown=false): # for name in files: # print(root,name) # for name in dirs: # print(root,name) # for root, dirs, files in os.walk(database, topdown=false): # for name in files: # print(os.path.join(root, name)) # for name in dirs: # print(os.path.join(root, name))
#:coding:utf-8 import socketserver, os, subprocess, hashlib from modules.user import login from conf import settings class mytcphandler(socketserver.baserequesthandler): def handle(self): try: while true: '''认证开始''' user_dict = self.request.recv(1024).decode('utf-8') msg, tag, config, db_path, name = login(user_dict) # 返回认证状态及数据 self.config = config # 为对象创建属性 self.db_path = db_path # 对象家目录 self.now_path = db_path # 对象当前路径 self.name = name state = ('%s:%s' % (msg, tag)) # 认证状态 if msg == false: self.request.send(state.encode('utf-8')) continue self.request.send(state.encode('utf-8')) while true: '''交互开始''' cmd = self.request.recv(1024).decode('utf-8') if len(cmd) == 0: break cmd_cmd = cmd.split()[0] # 接收cmd命令按空格切分得到第一个值 if hasattr(self, cmd_cmd): # 判断cmd命令存不存在类中 func = getattr(self, cmd_cmd) # 字符串调用类方法 func(cmd) except exception as f: # 针对windows print(f) def help(self, cmd): cmd_dict = ''' -------------------------帮助文档---------------------------------- 命令 说明 示例 cd 切换目录(public公共目录) cd dirname(目录名称) ls 查看当前目录下所有文件 ls pwd 查看当前路径 pwd get 下载文件 get filename(文件名) put 上传文件 put filename(文件名) mkdir 创建目录(当前路径下) mkdir dirname(目录名) ''' if len(cmd.split()) > 1: res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd self.request.sendall(res.encode('utf-8')) else: self.request.sendall(cmd_dict.encode('utf-8')) def ls(self, cmd): if len(cmd.split()) > 1: res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd self.request.sendall(res.encode('utf-8')) else: obj = subprocess.popen('dir %s' % self.now_path, shell=true, stdout=subprocess.pipe ) res = obj.stdout.read() self.request.sendall(res) def pwd(self, cmd): if len(cmd.split()) > 1: res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd else: res = self.now_path self.request.sendall(res.encode('utf-8')) def mkdir(self, cmd): if len(cmd.split()) == 2: dir = cmd.split()[1] dir_path = self.now_path + r'\%s' % dir if not os.path.isdir(dir_path): # 目录不存在 os.mkdir(dir_path) res = '< %s >目录创建成功!!!' % dir else: res = '< %s >目录已存在!' % dir else: res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd self.request.sendall(res.encode('utf-8')) def cd(self, cmd): if len(cmd.split()) == 2: dir = cmd.split()[1] if dir != self.name and dir in self.config.sections(): res = '权限不足,不要瞎搞' elif dir == 'public': self.now_path = settings.public res = '< pulic >共享目录切换成功!!!' elif os.path.isdir(self.now_path + r'\%s' % dir): self.now_path += r'\%s' % dir res = '< %s >目录切换成功!!!' % dir elif dir == '..' and len(self.now_path) > len(self.db_path): self.now_path = os.path.dirname(self.now_path) res = '< 上一级 >目录切换成功!!!' else: res = '权限不足,或路径不正确' self.request.sendall(res.encode('utf-8')) else: res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd self.request.sendall(res.encode('utf-8')) def get(self, cmd): '''下载''' if len(cmd.split()) == 2: filename = cmd.split()[1] file_path = self.now_path + r'\%s' % filename if os.path.isfile(file_path): self.request.sendall('exist'.encode('utf-8')) # 交互2发送文件存在的信号 file_size = os.stat(file_path).st_size # 计算文件大小 res = self.request.recv(1024).decode('utf-8') # 交互3接收状态信息 if res.split(':')[0] == 'exist': # 客户端文件存在 client_size = int(res.split(':')[1]) if client_size < file_size: # 客户端文件支持续传 self.request.sendall('yes'.encode('utf-8')) # 分支交互1 file_size -= client_size else: self.request.sendall('no'.encode('utf-8')) return else: # 文件不存在时 client_size = 0 with open(file_path, 'rb') as f: self.request.sendall(str(file_size).encode('utf-8')) # 交互4发送文件大小 self.request.recv(1024) # 交互5接收一次 其实多余 怕粘包 f.seek(client_size) # 文件指针移动到客户端文件大小位置 m = hashlib.md5() for line in f: m.update(line) self.request.sendall(line) # 交互6for循环发送循环数据 self.request.sendall(m.hexdigest().encode('utf-8')) # 交互7发送服务端文件md5值 else: self.request.sendall('文件不存在哦'.encode('utf-8')) else: res = '< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd self.request.sendall(res.encode('utf-8')) def put(self, cmd): '''上传''' file_name = cmd.split()[1] file_path = self.now_path + r'\%s' % file_name self.request.sendall('已准备好上传服务'.encode('utf-8')) # 交互2发送确认通知 file_size = int(self.request.recv(1024).decode('utf-8')) # 交互3接收文件大小 quota_size = int(self.config.get(self.name, 'quota')) # 拿到用户的磁盘配额 used_size = self.__getdirsize(self.db_path) # 计算得到用户已使用的空间 remain_size = quota_size - used_size # 得到用户剩余空间 if file_size + used_size <= quota_size: self.request.sendall(('yes:%s' % remain_size).encode('utf-8')) # 交互4发送可以接收通知和用户剩余空间 with open(file_path, 'wb') as f: receive_size = 0 m = hashlib.md5() while receive_size < file_size: real_size = file_size - int(receive_size) # 计算剩余大小 if real_size > 1024: size = 1024 else: size = real_size data = self.request.recv(size) # 交互5循环接收数据 receive_size += len(data) f.write(data) m.update(data) server_md5 = m.hexdigest() client_md5 = self.request.recv(1024).decode('utf-8') # 交互6接收客户端文件md5值 if server_md5 == client_md5: self.request.sendall('\nmd5值相同,文件具有一致性,文件上传完成'.encode('utf-8')) # 交互7发送完成信息 else: self.request.sendall(('no:%s' % remain_size).encode('utf-8')) # 分支交互4 def __getdirsize(self, db_path): '''计算已使用的用户家目录大小''' size = 0 for root, dirs, files in os.walk(db_path): size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) return size
from conf import settings import os, configparser, hashlib def login(user_dict): userlist = user_dict.split(':') name = userlist[0] password = userlist[1] config = query_db() # 查询本地用户数据库 if config.has_section(name): # 判断有没有name true_name = config.get(name, 'name') # 取出本地数据库对应name的名字 true_pwd = hash(config.get(name, 'password')) # 取出本地数据库对应name的加密过的密码 if name == true_name and password == true_pwd: # 比对 db_path = os.path.join(settings.database + r'\%s' % name) return true, '恭喜%s,认证成功!!!' % name, config, db_path, name # 额 返回了五个值。。。。 else: return false, '用户名或密码错误', none, none, none else: return false, '用户名或密码错误', none, none, none def query_db(): '''查询本地数据库''' config = configparser.configparser() config.read(settings.user_db + r'\user.ini') return config def hash(password): s = hashlib.md5() s.update(password.encode('utf-8')) return s.hexdigest()
[mogu] name=mogu password=123 quota=10240000 [xiaoming] name=xiaoming password=123 quota=10240000 [zhangsan] name=zhangsan password=123 quota=10240000
#:coding:utf-8 import socketserver, configparser, os from modules import server from conf import settings def create_dir(): '''初始化生成本地数据库用户属主目录''' config = configparser.configparser() # configpasrser模块 config.read(settings.user_db + r'\user.ini') # 用户数据文件路径 for user_name in config.sections(): # 循环取值 user_path = settings.database + r'\%s' % user_name if not os.path.isdir(user_path): # 文件夹不存在则创建 os.mkdir(user_path) if __name__ == '__main__': create_dir() server = socketserver.threadingtcpserver(('127.0.0.1', 6666), server.mytcphandler) server.serve_forever()
使用说明:
命令 说明 示例 cd 切换目录(public公共目录) cd dirname(目录名称) ls 查看当前目录下所有文件 ls pwd 查看当前路径 pwd get 下载文件 get filename(文件名) put 上传文件 put filename(文件名) mkdir 创建目录(当前路径下) mkdir dirname(目录名)
所有用户信息都在 user_db里的user.ini 文件里
客户端
|-------database(客户端数据库)本来想不建得,然后东西太多有点乱
|
|-------start.py 客户端程序的启动文件
客户端:
# :coding:utf-8 import socket, hashlib, os, sys base_dir = os.path.dirname(__file__) db_path = os.path.join(base_dir, 'database') def hash(password): s = hashlib.md5() s.update(password.encode('utf-8')) return s.hexdigest() class ftpclient: '''ftp客户端''' def __init__(self, ip_port): self.ip_port = ip_port def __connect(self): '''连接服务器''' self.client = socket.socket(socket.af_inet, socket.sock_stream) self.client.connect(self.ip_port) def __start(self): '''程序开始''' self.__connect() while true: '''认证''' name = input('用户名:').strip() pwd = input('密码:').strip() pwd = hash(pwd) user_dict = ('%s:%s' % (name, pwd)) self.client.sendall(user_dict.encode('utf-8')) state = self.client.recv(1024).decode('utf-8') if state.split(':')[0] == 'true': print(state.split(':')[1]) self.__interaction(name) else: print(state.split(':')[1]) def __interaction(self, name): '''交互开始''' while true: cmd = input('[%s]>>:' % name).strip() if len(cmd) == 0: continue cmd_cmd = cmd.split()[0] # 按照空格切分输入的命令 if hasattr(self, cmd_cmd): # 如果类中存在对应方法则执行 func = getattr(self, cmd_cmd) func(cmd) else: print('< %s >不是内部或外部命令,也不是可运行的程序或批处理文件。' '可查看帮助文档(help)' % cmd) def help(self, cmd): '''帮助命令''' self.client.sendall(cmd.encode('utf-8')) print(self.client.recv(1024).decode('utf-8')) def ls(self, cmd): '''查看当前路径文件命令''' self.client.sendall(cmd.encode('utf-8')) print(self.client.recv(2048).decode('gbk')) def pwd(self, cmd): '''显示当前路径命令''' self.client.sendall(cmd.encode('utf-8')) print(self.client.recv(1024).decode('utf-8')) def mkdir(self, cmd): '''创建目录''' self.client.sendall(cmd.encode('utf-8')) print(self.client.recv(1024).decode('utf-8')) def cd(self, cmd): self.client.sendall(cmd.encode('utf-8')) print(self.client.recv(1024).decode('utf-8')) def get(self, cmd): '''下载''' self.client.sendall(cmd.encode('utf-8')) # 1 交互 res = self.client.recv(1024).decode('utf-8') # 2 交互 if res == 'exist': filename = cmd.split()[1] if os.path.isfile(db_path + r'\%s' % filename): # 如果文件存在 receive_size = os.stat(db_path + r'\%s' % filename).st_size # 已接收文件大小 self.client.sendall(('exist:%s' % receive_size).encode('utf-8')) # 交互3发送状态和大小 state = self.client.recv(1024).decode('utf-8') # 分支交互1 if state == 'yes': print('文件续传成功,正在下载') else: print('文件完整,无法进行下载') return else: receive_size = 0 # 文件不存在时为0 self.client.sendall('no:0'.encode('utf-8')) # 交互3发送状态 file_size = int(self.client.recv(1024).decode('utf-8')) # 交互4接收文件大小 self.client.sendall('receive'.encode('utf-8')) # 交互5此交互其实多余,但是怕粘包 with open(db_path + r'\%s' % filename, 'ab') as f: file_size += int(receive_size) # 计算总文件大小 m = hashlib.md5() # md5 while receive_size < file_size: # 接收文件大小 < 总文件大小 real_size = file_size - int(receive_size) # 计算剩余大小 if real_size > 1024: size = 1024 else: size = real_size data = self.client.recv(size) # 交互6 开始循环接收文件 receive_size += len(data) f.write(data) # 追加写入 m.update(data) self.__progress(receive_size, file_size) # 进度条啦 client_md5 = m.hexdigest() # 客户端新文件md5值 server_md5 = self.client.recv(1024).decode('utf-8') # 交互7 接收服务端文件md5值 if client_md5 == server_md5: print('\nmd5值相同,文件具有一致性,文件下载完成') else: print(res) def put(self, cmd): '''上传''' if len(cmd.split()) == 2: file_name = cmd.split()[1] file_path = db_path + r'\%s' % file_name if os.path.isfile(file_path): # 客户端本地是否有文件 self.client.sendall(cmd.encode('utf-8')) # 交互1 print(self.client.recv(1024).decode('utf-8')) # 交互2收到确认通知 file_size = os.stat(file_path).st_size # 计算本地文件大小 self.client.sendall(str(file_size).encode('utf-8')) # 交互3发送文件大小 res = self.client.recv(1024).decode('utf-8') # 交互4接收确认信息和可用空间 remain_size = int(res.split(':')[1]) if res.split(':')[0] == 'yes': print('开始上传,当前剩余空间%sm' % (round(remain_size / 1024000))) # 四舍五入 with open(file_path, 'rb') as f: m = hashlib.md5() for line in f: m.update(line) send_size = f.tell() # 返回文件的当前位置 self.client.sendall(line) # 交互5for循环发送文件数据 self.__progress(send_size, file_size) self.client.sendall(m.hexdigest().encode('utf-8')) # 交互6发送本地文件md5值 print(self.client.recv(1024).decode('utf-8')) # 交互7 接收完成信息 else: print('空间不足哦,无法上传,当前剩余空间%sm' % (round(remain_size / 1024000))) else: print('< %s > 文件不存在哦' % file_name) else: print('< %s > 不是内部或外部命令,也不是可运行的程序或批处理文件,可查看帮助文档(help)' % cmd) def __progress(self, recv_size, data_size, width=70): ''' =========进度条啦没整明白========== # data_size = 9292 # recv_size = 0 # while recv_size < data_size: # time.sleep(0.1) # 模拟数据的传输延迟 # recv_size += 1024 # 每次收1024 # # percent = recv_size / data_size # 接收的比例 # progress(percent, width=70) # 进度条的宽度70''' percent = float(recv_size) / float(data_size) if percent >= 1: percent = 1 show_str = ('[%%-%ds]' % width) % (int(width * percent) * '>') print('\r%s %d%%' % (show_str, int(100 * percent)), file=sys.stdout, flush=true, end='') # print(ftpclient.__dict__) if __name__ == '__main__': ip_port = ('127.0.0.1', 6666) client = ftpclient(ip_port) client._ftpclient__start()
目前此程序还有些问题,例如cd 命令 以及如何在客户端显示家目录为根目录 等问题