Loading

Python--FTP练习

Python填坑之路-FTP

要求:

  1. 用户加密认证
  2. 允许同时多用户登录
  3. 每个用户有自己的家目录 ,且只能访问自己的家目录
  4. 对用户进行磁盘配额,每个用户的可用空间不同(未做)
  5. 允许用户在ftp server上随意切换目录
  6. 允许用户查看当前目录下文件
  7. 允许上传和下载文件,保证文件一致性
  8. 文件传输过程中显示进度条
  9. 附加功能:支持文件的断点续传(未实现)

服务端设置的目录:

# -*- coding:UTF-8 -*-
import os
import sys
sys.path.append(os.path.dirname(os.getcwd()))

from core import main

if __name__ == '__main__':
    main.ArgvHandler()
bin-ftp_server.py
# -*- 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)
conf-settings.py
# -*- 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)
core-log.py
# -*- 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()
core-main.py
# -*- 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
core-server.py

客户端:

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()
ftp_client.py

使用说明:

服务端windows上pycharm启动方式

服务端linux上终端启动方式

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

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

 

posted @ 2018-12-28 18:17  别来无恙-  阅读(303)  评论(0)    收藏  举报