Python--FTP练习
Python填坑之路-FTP
要求:
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同(未做)
- 允许用户在ftp server上随意切换目录
- 允许用户查看当前目录下文件
- 允许上传和下载文件,保证文件一致性
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传(未实现)
服务端设置的目录:

# -*- coding:UTF-8 -*- import os import sys sys.path.append(os.path.dirname(os.getcwd())) from core import main if __name__ == '__main__': main.ArgvHandler()
# -*- coding:UTF-8 -*- import os BASE_DIR = os.path.dirname(os.getcwd()) ACCOUNT_PATH = os.path.join(BASE_DIR, "conf", "accounts.ini") LOG_PATH = os.path.join(BASE_DIR, "logger") if os.name == "nt": IP_PORT = ('', 8088) elif os.name == "posix": IP_PORT = ('127.0.0.1', 8888)
# -*- coding:UTF-8 -*- import os import time import logging import inspect from logging.handlers import RotatingFileHandler from conf import settings handlers = {logging.NOTSET: os.path.join(settings.LOG_PATH, 'notset.log'), logging.DEBUG: os.path.join(settings.LOG_PATH, 'debug.log'), logging.INFO: os.path.join(settings.LOG_PATH, 'info.log'), logging.WARNING: os.path.join(settings.LOG_PATH, 'warning.log'), logging.ERROR: os.path.join(settings.LOG_PATH, 'error.log'), logging.CRITICAL: os.path.join(settings.LOG_PATH, 'command.log'), } date = time.strftime("%Y_%m_%d", time.localtime()) def createHandlers(): logLevels = handlers.keys() for level in logLevels: path = os.path.abspath(handlers[level]) handlers[level] = RotatingFileHandler(path, maxBytes=10000, backupCount=5, encoding='utf-8') # 加载模块时创建全局变量 createHandlers() class TNLog(object): def printfNow(self): return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) def __init__(self, level=logging.NOTSET): self.__loggers = {} logLevels = handlers.keys() for level in logLevels: logger = logging.getLogger(str(level)) # 如果不指定level,获得的handler似乎是同一个handler? logger.addHandler(handlers[level]) logger.setLevel(level) self.__loggers.update({level: logger}) def getLogMessage(self, level, message): frame, filename, lineNo, functionName, code, unknowField = inspect.stack()[2] '''日志格式:[时间] [类型] [记录代码] 信息''' return "[%s] [%s] [%s - %s - %s] %s" % (self.printfNow(), level, filename, lineNo, functionName, message) def info(self, message): message = self.getLogMessage("info", message) self.__loggers[logging.INFO].info(message) def error(self, message): message = self.getLogMessage("error", message) self.__loggers[logging.ERROR].error(message) def warning(self, message): message = self.getLogMessage("warning", message) self.__loggers[logging.WARNING].warning(message) def debug(self, message): message = self.getLogMessage("debug", message) self.__loggers[logging.DEBUG].debug(message) def cmd(self, message): message = self.getLogMessage("critical", message) self.__loggers[logging.CRITICAL].critical(message)
# -*- coding:UTF-8 -*- import optparse import socketserver from conf import settings from core.server import ServerHandler class ArgvHandler(): def __init__(self): self.op = optparse.OptionParser() options, args = self.op.parse_args() self.verify_args(options, args) def verify_args(self, options, args): """定义对参数验证的方法""" cmd = args[0] if hasattr(self, cmd): func = getattr(self, cmd) func() def start(self): print("%s FTP Service Started %s" % ("*"*10, "*"*10)) s = socketserver.ThreadingTCPServer((settings.IP_PORT), ServerHandler) s.serve_forever()
# -*- coding:UTF-8 -*- import socketserver import json import configparser import os import struct from conf import settings from core.log import TNLog STATUS_CODE = { 250: "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}", 251: "Invalid cmd ", 252: "Invalid auth data", 253: "Wrong username or password", 254: "Passed authentication", 255: "Filename doesn't provided", 256: "File doesn't exist on server", 257: "ready to send file", 258: "md5 verification", 301: "User already exists", 302: "Two password inconsistencies", 303: "login was successful", 800: "the file exist, but not enough, is continue? ", 801: "the file exist !", 802: "ready to receive datas", 803 : "file does not exist", 900: "md5 valdate success" } logger = TNLog() class ServerHandler(socketserver.BaseRequestHandler): def handle(self): try: lens = self.request.recv(4) lens = struct.unpack('i', lens)[0] data = self.request.recv(lens).decode('utf-8') if data == "regiest": logger.info("客户端 %s 注册用户" % self.client_address[0]) self.regiest() else: while True: data = self.request.recv(1024).strip() data = json.loads(data.decode('utf-8')) # 得到的是客户端发送过来的字典 ''' { "action":"auth", "username":"xiaobai", "pwd":123, } ''' # logger.info("来自%s登录的%s执行命令%s" % (self.client_address[0], self.user, data['action'])) if data.get("action"): if hasattr(self, data.get("action")): func = getattr(self, data.get("action")) func(**data) else: print("Invalid cmd") else: print("Invalid cmd") except: logger.error("%s 远程客户端意外关闭" % self.client_address[0]) def send_response(self,status_code): """定义发送状态码的函数""" response = {"status_code": status_code} self.request.sendall(json.dumps(response).encode('utf-8')) def auth(self, **data): username = data['username'] password = data['password'] user = self.authenticate(username, password) if user: self.send_response(254) # 登录成功返回给客户端对相应的状态码254 print("来自%s的%s登录成功" % (self.client_address[0], user)) logger.info("来自%s的%s登录成功" % (self.client_address[0], user)) else: self.send_response(253) # 登录失败返回给客户端对相应的状态码253 logger.info("来自%s的%s登录失败" % (self.client_address[0], user)) def authenticate(self, user, pwd): """定义对用户名和密码进行验证的函数""" cfg = configparser.ConfigParser() cfg.read(settings.ACCOUNT_PATH) if cfg.has_section(user): # 判断用户是否存在 if cfg[user]['Password'] == pwd: # 如果存在进行密码认证 self.user = user self.mainPath = os.path.join(settings.BASE_DIR, "home", self.user) # 用户家目录 return user def send_ret(self, ret): """定义用来发送执行命令后发送结果给客户端的函数""" self.request.sendall(ret.encode('utf-8')) def regiest(self): '''定义注册的函数''' cfg = configparser.ConfigParser() cfg.read(settings.ACCOUNT_PATH) json_userinfo = self.request.recv(1024).decode('utf-8') userinfo = json.loads(json_userinfo) user = userinfo['username'] pwd = userinfo['password'] if not cfg.has_section(user): cfg.add_section(user) cfg.set(user, "Password", pwd) cfg.write(open(settings.ACCOUNT_PATH, 'w')) userhome = os.path.join(settings.BASE_DIR, "home", user) os.mkdir(userhome) cfg[user] = { "Password": pwd, } self.send_response(303) logger.info("新用户:%s 注册成功"%user) else: self.send_response(301) logger.error("%s注册失败,用户已存在"%user) def get(self, **data): """定义下载文件的函数""" file_name = data['file_name'] abs_path = os.path.join(self.mainPath, file_name) # 拿到服务器文件的路径 file_size = None if os.path.exists(abs_path): # 如果文件存在,需要先发送文件的大小返回给客户端 file_size = os.stat(abs_path).st_size self.send_ret(str(file_size)) else: self.send_ret("803") logger.info("%s 下载文件失败 %s不存在" % (self.user, file_name)) choice = self.request.recv(1024).decode('utf-8') has_sent = 0 if choice == "Y": f = open(abs_path, 'rb') logger.info("%s 下载文件%s成功" %(self.user, file_name)) else: return while has_sent < file_size: data = f.read(1024) self.request.sendall(data) has_sent += len(data) f.close() def put(self, **data): """定义上传文件的函数""" file_name = data["file_name"] file_size = data["file_size"] target_path = data["target_path"] if not target_path: # 如果客户端没有指定上传目录,那么自动创建upload目录, 并上传在该目录 target_path = "upload" abs_dir = os.path.join(self.mainPath, target_path) abs_path = os.path.join(self.mainPath, target_path, file_name) if not os.path.exists(abs_dir): os.mkdir(abs_dir) logger.info("%s 上传文件时,自动创建目录 %s" % (self.user, target_path)) has_received = 0 if os.path.exists(abs_path): # 如果文件存在,取到文件的大小和发送文件的大小比较 file_has_size = os.stat(abs_path).st_size if file_has_size < file_size: # 断点续传 self.send_ret("800") choice = self.request.recv(1024).decode('utf-8') if choice == "Y": self.send_ret(str(file_has_size)) has_received += file_has_size f = open(abs_path, 'ab') logger.info("%s 上传文件%s,%s已经存在,但是不完整大小(%s),开始断点续传" % (self.user, file_name, file_name, file_has_size)) else: f = open(abs_path, 'wb') else: # 如果大小一致,那么直接返回文件已存在 self.send_ret("801") logger.info("%s 上传文件失败,%s已存在" % (self.user, file_name)) return # 如果文件不存在, 那么直接上传 else: self.send_ret("802") f = open(abs_path, 'wb') while has_received < file_size: try: data = self.request.recv(1024) except Exception as e: break f.write(data) has_received += len(data) f.close() logger.info("%s 上传文件%s" % (self.user, file_name)) def ls(self, **data): file_list = os.listdir(self.mainPath) file_str = "\n".join(file_list) if not len(file_list): file_str = "<empty dir>" self.send_ret(file_str) logger.cmd("%s command: [ls]" % self.user) def cd(self, **data): """定义切换目录的函数""" dirname = data.get("dirname") if dirname == ".." or dirname == "/": if self.mainPath == os.path.join(settings.BASE_DIR, "home", self.user): self.mainPath = os.path.join(settings.BASE_DIR, "home", self.user) self.send_ret(self.mainPath) else: self.mainPath = os.path.dirname(self.mainPath) self.send_ret(self.mainPath) logger.cmd("%s command: [cd ..]" % self.user) else: path = os.path.join(self.mainPath, dirname) if os.path.exists(path): self.mainPath = os.path.join(self.mainPath, dirname) logger.cmd("%s command: [cd %s]" % (self.user, dirname)) self.send_ret(self.mainPath) else: self.send_ret("cmd:cd %s: No such file or directory" % dirname) def pwd(self, **data): """定义常看当前所在位置的函数""" # os.path.basename(settings.BASE_DIR) # 拿到FTP-server # self.mainPath.split(os.path.basename(settings.BASE_DIR)) # print(self.mainPath.split(os.path.basename(settings.BASE_DIR))[1]) # \home\aa\file pwd_path = self.mainPath.split(os.path.basename(settings.BASE_DIR))[1] self.send_ret(pwd_path) def mkdir(self, **data): dirname = data.get("dirname") path = os.path.join(self.mainPath, dirname) if not os.path.exists(path): if "/" in dirname: os.makedirs(path) logger.cmd("%s command: [mkdir %s]" % (self.user, dirname)) else: os.mkdir(path) logger.cmd("%s command: [mkdir %s]" % (self.user, dirname)) self.send_ret("create success") else: self.send_ret(("%s exist" % dirname)) logger.error("%s 创建文件夹失败 %s 文件已存在]" % (self.user, dirname)) def q(self, **data): self.send_ret("q") logger.info('%s 退出登录' % self.user) print(self.user, "已退出") def h(self, **data): pass
客户端:
import optparse import socket import json import os import sys import time import struct STATUS_CODE = { 250: "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}", 251: "Invalid cmd ", 252: "Invalid auth data", 253: "Wrong username or password", 254: "Passed authentication", 255: "Filename doesn't provided", 256: "File doesn't exist on server", 257: "ready to send file", 258: "md5 verification", 301: "User already exists", 302: "Two password inconsistencies", 303: "login was successful", 800: "the file exist, but not enough, is continue? ", 801: "the file exist !", 802: "ready to receive datas", 803 : "file does not exist", 900: "md5 valdate success" } class ClientHandler(): def __init__(self): self.op = optparse.OptionParser() self.op.add_option('-s', '--server', dest='server') self.op.add_option('-P', '--port', dest='port') self.op.add_option('-u', '--username', dest='username') self.op.add_option('-p', '--password', dest='password') self.options, self.args = self.op.parse_args() self.verify_args(self.options, self.args) self.make_connection() # 服务器建立连接 self.mainPath = os.path.dirname(os.path.abspath(__file__)) self.last = 0 # 进度条所使用的变量 def verify_args(self, options, args): '''定义解析参数的函数''' server = options.server port = options.port if int(port)>0 and int(port)<65535: return True else: exit("the port is in 0-65535") def make_connection(self): '''定义和服务端连接的函数''' self.sock = socket.socket() self.sock.connect((self.options.server, int(self.options.port))) def interactive(self): if self.authenticate(): print("begin to interactive......") while True: cmd_info = input("[%s@ %s]"%(self.user, self.current_dir)).strip() # put filename cmd_list = cmd_info.split() if hasattr(self, cmd_list[0]): cmd_func = getattr(self, cmd_list[0]) cmd_func(*cmd_list) else: print("输入错误") def authenticate(self): '''定义判断用户是否启动时输入用户名和密码的函数,如果输入则直接进行登录,如果没有输入,那么让用户选择是进行注册还是登录''' if self.options.username is None or self.options.password is None: options = ["注册", "登录", "退出"] for index, item in enumerate(options, 1): print(index, item) option = input("Please choose [1/2/3]:>>> ") if option == "1": lens = struct.pack('i', len("regiest")) self.sock.send(lens) self.sock.send("regiest".encode('utf-8')) self.regiest() elif option == "2": lens = struct.pack('i', len("login")) self.sock.send(lens) self.sock.send("login".encode('utf-8')) username = input("username:>>> ") password = input("password:>>> ") return self.get_auth_result(username, password) elif option == "3": exit("Bye-bye") else: exit("Option error ...") else: lens = struct.pack('i', len("login")) self.sock.send(lens) self.sock.send("login".encode('utf-8')) return self.get_auth_result(self.options.username, self.options.password) def regiest(self): '''定义注册的函数''' username = input("UserName:>>> ").strip() password = input("Password:>>> ").strip() RepeatPassword= input("RepeatPassword:>>> ").strip() if RepeatPassword != password: exit(STATUS_CODE[302]) userinfo = {"username": username, "password": RepeatPassword} json_userinfo = json.dumps(userinfo) bytes_userinfo = json_userinfo.encode('utf-8') self.sock.send(bytes_userinfo) response = self.response() if response["status_code"] == 301: print(STATUS_CODE[301]) elif response["status_code"] == 303: print(STATUS_CODE[303]) def response(self): '''定义接收服务端回应的消息的函数''' data = self.sock.recv(1024).decode('utf-8') data = json.loads(data) return data def get_auth_result(self, user, pwd): '''定义真正验证用户名和密码的函数''' data = { "action": "auth", "username": user, "password": pwd, } self.sock.send(json.dumps(data).encode('utf-8')) response = self.response() if response["status_code"] == 254: self.user = user self.current_dir = user logo = " welcome %s Today is %s".center(30) % (user, time.strftime("%Y-%m-%d", time.localtime())) print("*" * len(logo)) print(logo) print() print("h --命令手册".rjust(30)) print("*" * len(logo)) # print(STATUS_CODE[254]) return True else: print(STATUS_CODE[response["status_code"]]) def show_progress(self, has, total): """定义进度条的函数""" rate = float(has)/float(total) rate_num = int(rate*100) # sys.stdout.write("%s%% %s\r" % (rate_num, "#" * rate_num)) if self.last != rate_num: sys.stdout.write("%s%% %s\r" % (rate_num, "#"*rate_num)) self.last = rate_num def get(self, *cmd_list): """定义下载文件的函数""" # get 12.jpg target_path = filename action, target_path = cmd_list data = { "action": "get", "file_name": target_path } self.sock.send(json.dumps(data).encode('utf-8')) is_exist = self.sock.recv(1024).decode('utf-8') # 接收到的可能是文件大小,也可能接收到文件不存在返回的状态码 ################################### # 如果得到的是文件大小,那么开始接收,如果得到的文件不存在就不做任何事情 if is_exist == "803": self.sock.send("N".encode('utf-8')) print(STATUS_CODE[803]) return else: self.sock.send("Y".encode('utf-8')) has_received = 0 f = open(target_path, 'wb') while has_received < int(is_exist): abc = self.sock.recv(1024) f.write(abc) has_received += len(abc) self.show_progress(has_received, is_exist) print("get success!") def h(self, *cmd_list): """定义帮助手册""" data = {"action": "h"} self.sock.sendall(json.dumps(data).encode('utf-8')) print('''命令帮助手册: put filename target --上传文件 Example:put 1.txt dir1; 如果不指定target, 默认上传在upload目录里面 get filename --下载文件 Example:get 1.txt ls --查看当前目录所有文件 cd --切换目录 Example:cd dir1 -进入dir1目录; cd .. -返回上一级目录 mkdir --创建文件夹 Example:mkdir dir2 -创建一个目录; mkdir dir3/dir1/dir2 -创建层级目录 q --退出登录''') def put(self, *cmd_list): """定义上传文件的函数""" # put 12.jpg images try: action, local_path, target_path = cmd_list except ValueError as e: action, local_path = cmd_list target_path = None local_path = os.path.join(self.mainPath, local_path) file_name = os.path.basename(local_path) file_size = os.stat(local_path).st_size data = { "action":"put", "file_name":file_name, "file_size":file_size, "target_path": target_path } self.sock.send(json.dumps(data).encode('utf-8')) is_exist = self.sock.recv(1024).decode('utf-8') # 拿到服务端返回的文件是否存在的信息 ################################### has_sent = 0 if is_exist == "800": # 文件存在但是不完整 # print(STATUS_CODE[is_exist]) choice = input("the file exist, but not enough, is continus?[Y/N]").strip() if choice.upper() == "Y": self.sock.sendall("Y".encode('utf-8')) continue_position = self.sock.recv(1024).decode('utf-8') has_sent+=int(continue_position) else: self.sock.sendall("N".encode('utf-8')) elif is_exist == "801": # 文件完全存在 print(STATUS_CODE[801]) return f = open(local_path, 'rb') f.seek(has_sent) while has_sent < file_size: data = f.read(1024) self.sock.sendall(data) has_sent += len(data) self.show_progress(has_sent, file_size) f.close() print("put success!") def ls(self, *cmd_list): data = { "action": "ls", } self.sock.sendall(json.dumps(data).encode('utf-8')) data = self.sock.recv(1024).decode('utf-8') print(data) def cd(self, *cmd_list): # cd images data = { "action": "cd", "dirname":cmd_list[1] } self.sock.sendall(json.dumps(data).encode('utf-8')) data = self.sock.recv(1024).decode('utf-8') if "No such file or directory" not in data: self.current_dir = os.path.basename(data) else: print(data) def pwd(self, *cmd_list): data = {"action": "pwd"} self.sock.sendall(json.dumps(data).encode('utf-8')) path = self.sock.recv(1024).decode('utf-8') print(path) def mkdir(self, *cmd_list): data = { "action": "mkdir", "dirname": cmd_list[1] } self.sock.sendall(json.dumps(data).encode('utf-8')) ret = self.sock.recv(1024).decode('utf-8') print(ret) def q(self, *cmd_list): data = { "action":"q" } self.sock.send(json.dumps(data).encode('utf-8')) ret = self.sock.recv(1024).decode('utf-8') if ret == "q": self.sock.close() exit() ch = ClientHandler() ch.interactive()
使用说明:
服务端windows上pycharm启动方式

服务端linux上终端启动方式

客户端指定服务器和端口链接

如果已有账号和密码可以直接登录

人生是条无名的河,是浅是深都要过;
人生是杯无色的茶,是苦是甜都要喝;
人生是首无畏的歌,是高是低都要唱。

浙公网安备 33010602011771号