作业:开发一个支持多用户在线的FTP程序
要求:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传
之前作业的链接地址:https://www.cnblogs.com/hukey/p/8909046.html 这次的重写是对上次作业的补充,具体实现功能点如下:
README
# 作者介绍:
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网络模块 # 功能实现:
. 实现了用户注册和登录验证(新增)。
. 用户注册时,将用户名添加到 conf/user.list里并创建home/[username],为每个用户生成独立的数据库文件 db/[username].db
. 每个用户的磁盘配额为10M, 在conf/settings.py 中声明, 可以修改
. 本程序适用于windows,命令:cd / mkdir / pwd / dir / put / get
. 实现了get下载续传的功能:
服务器存在文件, 客户端不存在,直接下载;
服务器存在文件, 客户端也存在文件,比较大小, 一致则不传,不一致则追加续传;
. 实现日志记录(新增) # 状态码: 登录验证(用户名或密码错误)
注册验证(注册的用户名已存在)
命令不正确
空间不足
续传
get(客户端文件存在) 登录成功
注册成功
命令执行成功
文件一致 系统交互码
README
程序结构
具体代码实现
1. ftp客户端程序
#!/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 == '':
print('\033[32;1m登录成功.\033[0m')
self.interactive()
elif status_code == '':
print('\033[32;1m注册成功.\033[0m')
elif status_code == '':
print('\033[31;1m用户名或密码错误.\033[0m')
elif status_code == '':
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 == '':
self.client.sendall(b'')
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 == '':
self.client.sendall(b'')
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 == '':
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 == '':
filename = command.split()[1]
if os.path.isfile(filename):
self.client.sendall(b'')
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 == '':
print('续传.')
response = self.client.sendall(b'')
elif status_code == '':
print('文件一致.')
return
else:
self.client.sendall(b'')
has_send_data = 0 file_size = int(self.client.recv(1024).decode())
self.client.sendall(b'')
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()
ftp_client.py
2. ftp服务端程序
(1)ftp启动程序
#!/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()
start.py
(2)conf配置文件
#!/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)
settings.py
(3)modules 核心模块
#!/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 ''
else:
Logger.warning('[%s]注册用户名已存在。' % self.user)
return '' 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
auth.py
#!/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)
log.py
#!/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'')
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'')
except ConnectionResetError as e:
print('Error:', e) def dir(self, command):
if len(command.split()) == 1:
Logger.info('[%s] 执行成功.' % command)
self.request.sendall(b'')
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'') def pwd(self, command):
if len(command.split()) == 1:
self.request.sendall(b'')
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'') 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'')
response = self.request.recv(1024)
os.makedirs(dir_path)
else:
Logger.warning('[%s] 命令格式错误.' % command)
self.request.sendall(b'') 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'')
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'')
response = self.request.recv(1024)
if dir_name != '.' and dir_name != '..':
self.user_current_path = dir_path
else:
self.request.sendall(b'')
else:
Logger.warning('[%s] 命令格式错误.' % command)
self.request.sendall(b'') def put(self, command):
filename = command.split()[1]
file_path = os.path.join(self.user_current_path, filename)
response = self.request.sendall(b'')
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'')
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'') 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'')
file_size = os.path.getsize(file_path)
status_code = self.request.recv(1024).decode()
if status_code == '':
self.request.sendall(b'')
recv_size = int(self.request.recv(1024).decode())
if file_size > recv_size:
self.request.sendall(b'')
respon = self.request.recv(1024)
elif file_size == recv_size:
self.request.sendall(b'')
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'')
socket_server.py
(4)其他目录
db/ - 注册成功后生成个人数据库文件
home/ - 注册成功后创建个人家目录
log/ - 日志文件目录
程序运行效果图
(1)注册、登录及命令的执行
client:
server:
(2)上传
(3)下载(续传功能)