做業:開發一個支持多用戶在線的FTP程序html
要求:python
以前做業的連接地址:http://www.javashuo.com/article/p-divtbnjb-g.html 此次的重寫是對上次做業的補充,具體實現功能點以下:shell
# 做者介紹: 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客戶端程序數據庫
#!/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服務端程序windows
(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()
(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)
(3)modules 核心模塊socket
#!/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
#!/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)
#!/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)其餘目錄ide
db/ - 註冊成功後生成我的數據庫文件 home/ - 註冊成功後建立我的家目錄 log/ - 日誌文件目錄
(1)註冊、登陸及命令的執行加密
client:
server:
(2)上傳
(3)下載(續傳功能)