[ python ] FTP作业进阶
作业:开发一个支持多用户在线的FTP程序
要求:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传
之前作业的链接地址:https://www.cnblogs.com/hukey/p/8909046.html 这次的重写是对上次作业的补充,具体实现功能点如下:
README
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 作者介绍: author: hkey # 博客地址: https://www.cnblogs.com/hukey/p/10182876.html # 功能实现: 作业:开发一个支持多用户在线的FTP程序 要求: 用户加密认证 允许同时多用户登录 每个用户有自己的家目录 ,且只能访问自己的家目录 对用户进行磁盘配额,每个用户的可用空间不同 允许用户在ftp server上随意切换目录 允许用户查看当前目录下文件 允许上传和下载文件,保证文件一致性 文件传输过程中显示进度条 附加功能:支持文件的断点续传 # 目录结构: FTP ├── ftp_client/ # ftp客户端程序 │ └── ftp_client.py # 客户端主程序 └── ftp_server/ # ftp服务端程序 ├── bin/ │ ├── __init__.py │ └── start.py ├── conf/ # 配置文件目录 │ ├── __init__.py │ ├── settings.py │ └── user.list # 记录注册用户名 ├── db/ # 用户数据库 ├── home/ # 用户家目录 ├── logs/ # 记录日志目录 └── modules/ # 程序核心功能目录 ├── auth.py # 用户认证(注册和登录) ├── __init__.py ├── log.py # 日志初始化类 └── socket_server.py # socket网络模块 # 功能实现: 1. 实现了用户注册和登录验证(新增)。 2. 用户注册时,将用户名添加到 conf/user.list里并创建home/[username],为每个用户生成独立的数据库文件 db/[username].db 2. 每个用户的磁盘配额为10M, 在conf/settings.py 中声明, 可以修改 3. 本程序适用于windows,命令:cd / mkdir / pwd / dir / put / get 4. 实现了get下载续传的功能: 服务器存在文件, 客户端不存在,直接下载; 服务器存在文件, 客户端也存在文件,比较大小, 一致则不传,不一致则追加续传; 5. 实现日志记录(新增) # 状态码: 400 登录验证(用户名或密码错误) 401 注册验证(注册的用户名已存在) 402 命令不正确 403 空间不足 405 续传 406 get(客户端文件存在) 200 登录成功 201 注册成功 202 命令执行成功 203 文件一致 000 系统交互码
程序结构
具体代码实现
1. ftp客户端程序
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Author: hkey import os, sys import socket class MyClient: def __init__(self, ip_port): self.client = socket.socket() self.ip_port = ip_port def connect(self): self.client.connect(self.ip_port) def start(self): self.connect() while True: print('注册(register)\n登录(login)') auth_type = input('>>>').strip() if not auth_type: continue if auth_type == 'register' or auth_type == 'login': user = input('用户名:').strip() pwd = input('密码:').strip() auth_info = '%s:%s:%s' % (auth_type, user, pwd) self.client.sendall(auth_info.encode()) status_code = self.client.recv(1024).decode() if status_code == '200': print('\033[32;1m登录成功.\033[0m') self.interactive() elif status_code == '201': print('\033[32;1m注册成功.\033[0m') elif status_code == '400': print('\033[31;1m用户名或密码错误.\033[0m') elif status_code == '401': print('\033[31;1m注册用户名已存在.\033[0m') else: print('[%s]Error!' % status_code) else: print('\033[31;1m输入错误,请重新输入.\033[0m') def interactive(self): while True: command = input('>>>').strip() if not command: continue command_str = command.split()[0] if hasattr(self, command_str): func = getattr(self, command_str) func(command) def dir(self, command): self.__universal_method_data(command) def pwd(self, command): self.__universal_method_data(command) def mkdir(self, command): self.__universal_method_none(command) def cd(self, command): self.__universal_method_none(command) def __universal_method_none(self, command): self.client.sendall(command.encode()) status_code = self.client.recv(1024).decode() if status_code == '202': self.client.sendall(b'000') else: print('[%s]Error!' % status_code) def __universal_method_data(self, command): self.client.sendall(command.encode()) status_code = self.client.recv(1024).decode() if status_code == '202': self.client.sendall(b'000') result = self.client.recv(4096) print(result.decode('gbk')) else: print('[%s]Error!' % status_code) def put(self, command): if len(command.split()) > 1: filename = command.split()[1] if os.path.isfile(filename): self.client.sendall(command.encode()) file_size = os.path.getsize(filename) response = self.client.recv(1024) self.client.sendall(str(file_size).encode()) status_code = self.client.recv(1024).decode() if status_code == '202': with open(filename, 'rb') as f: while True: data = f.read(1024) send_size = f.tell() if not data: break self.client.sendall(data) self.__progress(send_size, file_size, '上传中') else: print('\033[31;1m[%s]空间不足.\033[0m' % status_code) else: print('\033[31;1m[%s]文件不存在.\033[0m' % filename) else: print('\033[31;1m命令格式错误.\033[0m') def __progress(self, trans_size, file_size, mode): bar_length = 100 percent = float(trans_size) / float(file_size) hashes = '=' * int(percent * bar_length) spaces = ' ' * int(bar_length - len(hashes)) sys.stdout.write('\r%s %.2fM/%.2fM %d%% [%s]' % (mode, trans_size / 1048576, file_size / 1048576, percent * 100, hashes + spaces)) def get(self, command): self.client.sendall(command.encode()) status_code = self.client.recv(1024).decode() if status_code == '202': filename = command.split()[1] if os.path.isfile(filename): self.client.sendall(b'406') response = self.client.recv(1024) has_send_data = os.path.getsize(filename) self.client.sendall(str(has_send_data).encode()) status_code = self.client.recv(1024).decode() if status_code == '405': print('续传.') response = self.client.sendall(b'000') elif status_code == '203': print('文件一致.') return else: self.client.sendall(b'202') has_send_data = 0 file_size = int(self.client.recv(1024).decode()) self.client.sendall(b'000') with open(filename, 'ab') as f: while has_send_data != file_size: data = self.client.recv(1024) has_send_data += len(data) f.write(data) self.__progress(has_send_data, file_size, '下载中') else: print('[%s]Error!' % status_code) if __name__ == '__main__': ftp_client = MyClient(('localhost', 8080)) ftp_client.start()
2. ftp服务端程序
(1)ftp启动程序
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Author: hkey import os, sys BASE_DIR = os.path.dirname(os.getcwd()) sys.path.insert(0, BASE_DIR) from conf import settings from modules import socket_server if __name__ == '__main__': server = socket_server.socketserver.ThreadingTCPServer(settings.IP_PORT, socket_server.MyServer) server.serve_forever()
(2)conf配置文件
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Author: hkey import os BASE_DIR = os.path.dirname(os.getcwd()) HOME_PATH = os.path.join(BASE_DIR, 'home') LOG_PATH = os.path.join(BASE_DIR, 'logs') DB_PATH = os.path.join(BASE_DIR, 'db') USER_LIST_FILE = os.path.join(BASE_DIR, 'conf', 'user.list') LOG_SIZE = 102400 LOG_NUM = 5 LIMIT_SIZE = 10240000000 IP_PORT = ('localhost', 8080)
(3)modules 核心模块
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Author: hkey import os, sys import pickle from conf import settings from modules.log import Logger class Auth: def __init__(self, user, pwd): self.user = user self.pwd = pwd def register(self): user_list = Auth.file_oper(settings.USER_LIST_FILE, 'r').split('\n')[:-1] if self.user not in user_list: Auth.file_oper(settings.USER_LIST_FILE, 'a', self.user + '\n') user_home_path = os.path.join(settings.HOME_PATH, self.user) if not os.path.isdir(user_home_path): os.makedirs(user_home_path) user_dict = {'user': self.user, 'pwd': self.pwd, 'home_path': user_home_path, 'limit_size': settings.LIMIT_SIZE} user_pickle = pickle.dumps(user_dict) user_db_file = os.path.join(settings.DB_PATH, self.user) + '.db' Auth.file_oper(user_db_file, 'ab', user_pickle) Logger.info('[%s]注册成功。' % self.user) return '201' else: Logger.warning('[%s]注册用户名已存在。' % self.user) return '401' def login(self): user_list = Auth.file_oper(settings.USER_LIST_FILE, 'r').split('\n')[:-1] if self.user in user_list: user_db_file = os.path.join(settings.DB_PATH, self.user) + '.db' user_pickle = Auth.file_oper(user_db_file, 'rb') user_dict = pickle.loads(user_pickle) if self.user == user_dict['user'] and self.pwd == user_dict['pwd']: Logger.info('[%s]登录成功.' % self.user) return user_dict else: Logger.error('[%s]用户名或密码错误.' % self.user) else: Logger.warning('[%s]登录用户不存在.' % self.user) @staticmethod def file_oper(file, mode, *args): if mode == 'a' or mode == 'ab': data = args[0] with open(file, mode) as f: f.write(data) elif mode == 'r' or mode == 'rb': with open(file, mode) as f: data = f.read() return data
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Author: hkey import os, sys import logging.handlers from conf import settings class Logger: logger = logging.getLogger() formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S') logfile = os.path.join(settings.LOG_PATH, sys.argv[0].split('/')[-1].split('.')[0]) + '.log' fh = logging.handlers.RotatingFileHandler(filename=logfile, maxBytes=settings.LOG_SIZE, backupCount=settings.LOG_NUM, encoding='utf-8') ch = logging.StreamHandler() fh.setFormatter(formatter) ch.setFormatter(formatter) logger.setLevel(level=logging.INFO) logger.addHandler(fh) logger.addHandler(ch) @classmethod def info(cls, msg): cls.logger.info(msg) @classmethod def warning(cls, msg): cls.logger.warning(msg) @classmethod def error(cls, msg): cls.logger.error(msg)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Author: hkey import os import socketserver import subprocess from os.path import getsize, join from modules.auth import Auth from modules.log import Logger class MyServer(socketserver.BaseRequestHandler): def handle(self): try: while True: auth_info = self.request.recv(1024).decode() auth_type, user, pwd = auth_info.split(':') auth_user = Auth(user, pwd) if auth_type == 'register': status_code = auth_user.register() self.request.sendall(status_code.encode()) elif auth_type == 'login': user_dict = auth_user.login() if user_dict: self.request.sendall(b'200') self.user_current_path = user_dict['home_path'] self.user_home_path = user_dict['home_path'] self.user_limit_size = user_dict['limit_size'] while True: command = self.request.recv(1024).decode() command_str = command.split()[0] if hasattr(self, command_str): func = getattr(self, command_str) func(command) else: self.request.sendall(b'400') except ConnectionResetError as e: print('Error:', e) def dir(self, command): if len(command.split()) == 1: Logger.info('[%s] 执行成功.' % command) self.request.sendall(b'202') response = self.request.recv(1024) cmd_res = subprocess.Popen('dir %s' % self.user_current_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) stdout = cmd_res.stdout.read() stderr = cmd_res.stderr.read() result = stdout if stdout else stderr self.request.sendall(result) else: Logger.warning('[%s] 命令格式错误.' % command) self.request.sendall(b'402') def pwd(self, command): if len(command.split()) == 1: self.request.sendall(b'202') Logger.info('[%s] 执行成功.' % command) response = self.request.recv(1024) self.request.sendall(self.user_current_path.encode()) else: Logger.warning('[%s] 命令格式错误.' % command) self.request.sendall(b'402') def mkdir(self, command): if len(command.split()) > 1: dir_name = command.split()[1] dir_path = os.path.join(self.user_current_path, dir_name) if not os.path.isdir(dir_path): Logger.info('[%s] 执行成功.' % command) self.request.sendall(b'202') response = self.request.recv(1024) os.makedirs(dir_path) else: Logger.warning('[%s] 命令格式错误.' % command) self.request.sendall(b'402') def cd(self, command): if len(command.split()) > 1: dir_name = command.split()[1] dir_path = os.path.join(self.user_current_path, dir_name) if dir_name == '..' and len(self.user_current_path) > len(self.user_home_path): self.request.sendall(b'202') response = self.request.recv(1024) self.user_current_path = os.path.dirname(self.user_current_path) elif os.path.isdir(dir_path): self.request.sendall(b'202') response = self.request.recv(1024) if dir_name != '.' and dir_name != '..': self.user_current_path = dir_path else: self.request.sendall(b'403') else: Logger.warning('[%s] 命令格式错误.' % command) self.request.sendall(b'402') def put(self, command): filename = command.split()[1] file_path = os.path.join(self.user_current_path, filename) response = self.request.sendall(b'000') file_size = self.request.recv(1024).decode() file_size = int(file_size) used_size = self.__getdirsize(self.user_home_path) if self.user_limit_size > file_size + used_size: self.request.sendall(b'202') Logger.info('[%s] 执行成功.' % command) recv_size = 0 Logger.info('[%s] 文件开始上传.' % file_path) with open(file_path, 'wb') as f: while recv_size != file_size: data = self.request.recv(1024) recv_size += len(data) f.write(data) Logger.info('[%s] 文件上传完成.' % file_path) else: self.request.sendall(b'403') def __getdirsize(self, user_home_path): size = 0 for root, dirs, files in os.walk(user_home_path): size += sum([getsize(join(root, name)) for name in files]) return size def get(self, command): if len(command.split()) > 1: filename = command.split()[1] file_path = os.path.join(self.user_current_path, filename) if os.path.isfile(file_path): self.request.sendall(b'202') file_size = os.path.getsize(file_path) status_code = self.request.recv(1024).decode() if status_code == '406': self.request.sendall(b'000') recv_size = int(self.request.recv(1024).decode()) if file_size > recv_size: self.request.sendall(b'405') respon = self.request.recv(1024) elif file_size == recv_size: self.request.sendall(b'203') print('一致.') return else: recv_size = 0 self.request.sendall(str(file_size).encode()) resonse = self.request.recv(1024) with open(file_path, 'rb') as f: f.seek(recv_size) while True: data = f.read(1024) if not data: break self.request.sendall(data) else: self.request.sendall(b'402')
(4)其他目录
db/ - 注册成功后生成个人数据库文件 home/ - 注册成功后创建个人家目录 log/ - 日志文件目录
程序运行效果图
(1)注册、登录及命令的执行
client:
server:
(2)上传
(3)下载(续传功能)
本文作者:hukey
本文链接:https://www.cnblogs.com/hukey/p/10182876.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2016-12-27 Centos7 / RHEL 7 双网卡绑定