Python实现FTP

 一、程序介绍:

需求:

支持多用户在线的FTP程序

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

实现功能:
用户加密认证
允许同时多用户登录
每个用户有自己的家目录 ,且只能访问自己的家目录
允许上传和下载文件,保证文件一致性
文件传输过程中显示进度条

程序结构:

FTP服务端
FtpServer  #服务端主目录
├── bin  #启动目录
│   └── ftp_server.py #启动文件
├── conf  #配置文件目录
│   ├── accounts.cfg #用户存储
│   └── settings.py #配置文件
├── core  #程序主逻辑目录
│   ├── ftp_server.py #功能文件
│   └── main.py #主逻辑文件
├── home  #用户家目录
│   ├── test001  #用户目录
│   └── test002  #用户目录
└── log  #日志目录

FTP客户端
FtpClient  #客户端主目录
└── ftp_client.py #客户端执行文件

二、流程图

三、代码

#FtpServer代码

bin/ftp_server.py
#!/usr/bin/env python
#_*_coding:utf-8_*_

import os
import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)

from core import main

if __name__ == '__main__':
    main.ArvgHandler()

 
conf/accounts.cfg

[DEFAULT]

[test001]
Password = 123
Quotation = 100

[test002]
Password = 123
Quotation = 100

conf/settings.py
#!/usr/bin/env python
#_*_coding:utf-8_*_

import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

USER_HOME = "%s/home" % BASE_DIR
LOG_DIR = "%s/log" % BASE_DIR
LOG_LEVEL = "DEBUG"

ACCOUNT_FILE = "%s/conf/accounts.cfg" % BASE_DIR

HOST = "127.0.0.1"
PORT = 9999

core/ftp_server.py
#!/usr/bin/env python
#_*_coding:utf-8_*_

import socketserver
import json
import configparser
import os
import hashlib
from conf import settings

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",
}

'''
250:“无效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}”,
251:“无效的CMD”,
252:“验证数据无效”,
253:“错误的用户名或密码”,
254:“通过身份验证”,
255:“文件名不提供”,
256:“服务器上不存在文件”,
257:“准备发送文件”,
258:“md5验证”,
'''

class FTPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        '''接收客户端消息(用户,密码,action)'''
        while True:
            self.data = self.request.recv(1024).strip()
            print(self.client_address[0])
            print(self.data)
            # self.request.sendall(self.data.upper())

            if not self.data:
                print("client closed...")
                break
            data = json.loads(self.data.decode())  #接收客户端消息
            if data.get('action') is not None:  #action不为空
                print("---->", hasattr(self, "_auth"))
                if hasattr(self, "_%s" % data.get('action')): #客户端action 符合服务端action
                    func = getattr(self, "_%s" % data.get('action'))
                    func(data)
                else:  #客户端action 不符合服务端action
                    print("invalid cmd")
                    self.send_response(251)  # 251:“无效的CMD”
            else:  #客户端action 不正确
                print("invalid cmd format")
                self.send_response(250) # 250:“无效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}”

    def send_response(self,status_code,data=None):
        '''向客户端返回数据'''
        response = {'status_code':status_code,'status_msg':STATUS_CODE[status_code]}
        if data:
            response.update(data)
        self.request.send(json.dumps(response).encode())

    def _auth(self,*args,**kwargs):
        '''核对服务端 发来的用户,密码'''
        # print("---auth",args,kwargs)
        data = args[0]
        if data.get("username") is None or data.get("password") is None: #客户端的用户和密码有一个为空 则返回错误
            self.send_response(252)  # 252:“验证数据无效”

        user = self.authenticate(data.get("username"),data.get("password")) #把客户端的用户密码进行验证合法性
        if user is None: #客户端的数据为空 则返回错误
            self.send_response(253)  # 253:“错误的用户名或密码”
        else:
            print("password authentication",user)
            self.user = user
            self.send_response(254)  # 254:“通过身份验证”

    def authenticate(self,username,password):
        '''验证用户合法性,合法就返回数据,核对本地数据'''
        config = configparser.ConfigParser()
        config.read(settings.ACCOUNT_FILE)
        if username in config.sections():  #用户匹配成功
            _password = config[username]["Password"]
            if _password == password:  #密码匹配成功
                print("pass auth..",username)
                config[username]["Username"] = username
                return config[username]

    def _put(self,*args,**kwargs):
        "client send file to server"
        data = args[0]
        base_filename = data.get('filename')
        file_obj = open(base_filename, 'wb')
        data = self.request.recv(4096)
        file_obj.write(data)
        file_obj.close()

    def _get(self,*args,**kwargs):
        '''get 下载方法'''
        data = args[0]
        if data.get('filename') is None:
            self.send_response(255)  # 255:“文件名不提供”,
        user_home_dir = "%s/%s" %(settings.USER_HOME,self.user["Username"]) #当前连接用户的目录
        file_abs_path = "%s/%s" %(user_home_dir,data.get('filename'))  #客户端发送过来的目录文件
        print("file abs path",file_abs_path)

        if os.path.isfile(file_abs_path):  #客户端目录文件名 存在服务端
            file_obj = open(file_abs_path,'rb')  # 用bytes模式打开文件
            file_size = os.path.getsize(file_abs_path)  #传输文件的大小
            self.send_response(257,data={'file_size':file_size}) #返回即将传输的文件大小 和状态码

            self.request.recv(1)  #等待客户端确认

            if data.get('md5'): #有 --md5 则传输时加上加密
                md5_obj = hashlib.md5()
                for line in file_obj:
                    self.request.send(line)
                    md5_obj.update(line)
                else:
                    file_obj.close()
                    md5_val = md5_obj.hexdigest()
                    self.send_response(258,{'md5':md5_val})
                    print("send file done....")
            else:  #没有 --md5  直接传输文件
                for line in file_obj:
                    self.request.send(line)
                else:
                    file_obj.close()
                    print("send file done....")

        else:
            self.send_response(256) # 256:“服务器上不存在文件”=


    def _ls(self,*args,**kwargs):
        pass

    def _cd(self,*args,**kwargs):
        pass
    

if __name__ == '__main__':
    HOST, PORT = "127.0.0.1", 9999

core/main.py
#!/usr/bin/env python
#_*_coding:utf-8_*_

import optparse
from core.ftp_server import FTPHandler
import socketserver
from conf import settings

class ArvgHandler(object):
    def __init__(self):
        self.parser = optparse.OptionParser()
        # parser.add_option("-s","--host",dest="host",help="server binding host address")
        # parser.add_option("-p","--port",dest="port",help="server binding port")
        (options, args) = self.parser.parse_args()
        # print("parser",options,args)
        # print(options.host,options.port)
        self.verify_args(options, args)

    def verify_args(self,options,args):
        '''校验并调用相应功能'''
        if hasattr(self,args[0]):
            func = getattr(self,args[0])
            func()
        else:
            self.parser.print_help()

    def start(self):
        print('---going to start server---')

        server = socketserver.ThreadingTCPServer((settings.HOST, settings.PORT), FTPHandler)

        server.serve_forever()
#FtpClient代码

ftp_client.py


#!/usr/bin/env python
#_*_coding:utf-8_*_

import socket
import os
import sys
import optparse
import json
import hashlib

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",
}

class FTPClient(object):
    def __init__(self):
        parser = optparse.OptionParser()
        parser.add_option("-s","--server",dest="server",help="ftp server ip_addr")
        parser.add_option("-P","--port",type="int",dest="port",help="ftp server port")
        parser.add_option("-u","--username",dest="username",help="username")
        parser.add_option("-p","--password",dest="password",help="password")

        self.options,self.args = parser.parse_args()
        self.verify_args(self.options,self.args)
        self.make_connection()

    def make_connection(self):
        '''远程连接'''
        self.sock = socket.socket()
        self.sock.connect((self.options.server,self.options.port))

    def verify_args(self,options,args):
        '''校验参数合法性'''
        if options.username is not None and options.password is not None:  #用户和密码,两个都不为空
            pass
        elif options.username is None and options.password is None: #用户和密码,两个都为空
            pass
        else:  #用户和密码,有一个为空
            # options.username is None or options.password is None:  #用户和密码,有一个为空
            exit("Err: username and password must be provided together...")

        if options.server and options.port:
            # print(options)
            if options.port >0 and options.port <65535:
                return True
            else:
                exit("Err:host port must in 0-65535")

    def authenticate(self):
        '''用户验证,获取客户端输入信息'''
        if self.options.username:  #有输入信息 发到远程判断
            print(self.options.username,self.options.password)
            return self.get_auth_result(self.options.username,self.options.password)
        else:  #没有输入信息 进入交互式接收信息
            retry_count = 0
            while retry_count <3:
                username = input("username: ").strip()
                password = input("password: ").strip()
                return self.get_auth_result(username,password)
                # retry_count +=1

    def get_auth_result(self,user,password):
        '''远程服务器判断 用户,密码,action '''
        data = {'action':'auth',
                'username':user,
                'password':password,}

        self.sock.send(json.dumps(data).encode())  #发送 用户,密码,action 到远程服务器  等待远程服务器的返回结果
        response = self.get_response()  #获取服务器返回码
        if response.get('status_code') == 254: #通过验证的服务器返回码
            print("Passed authentication!")
            self.user = user
            return True
        else:
            print(response.get("status_msg"))

    def get_response(self):
        '''得到服务器端回复结果,公共方法'''
        data = self.sock.recv(1024)
        data = json.loads(data.decode())
        return data

    def interactive(self):
        '''交互程序'''
        if self.authenticate(): #认证成功,开始交互
            print("--start interactive iwth u...")
            while True: #循环 输入命令方法
                choice = input("[%s]:"%self.user).strip()
                if len(choice) == 0:continue
                cmd_list = choice.split()
                if hasattr(self,"_%s"%cmd_list[0]): #反射判断 方法名存在
                    func = getattr(self,"_%s"%cmd_list[0]) #反射 方法名
                    func(cmd_list)  #执行方法
                else:
                    print("Invalid cmd.")

    def _md5_required(self,cmd_list):
        '''检测命令是否需要进行MD5的验证'''
        if '--md5' in cmd_list:
            return True

    def show_progress(self,total):
        '''进度条'''
        received_size = 0
        current_percent = 0
        while received_size < total:
            if int((received_size / total) * 100) > current_percent :
                print("#",end="",flush=True)
                current_percent = (received_size / total) * 100
            new_size = yield
            received_size += new_size

    def _get(self,cmd_list):
        ''' get 下载方法'''
        print("get--",cmd_list)
        if len(cmd_list) == 1:
            print("no filename follows...")
            return
        #客户端操作信息
        data_header = {
            'action':'get',
            'filename':cmd_list[1],
        }

        if self._md5_required(cmd_list):  #命令请求里面有带 --md5
            data_header['md5'] = True  #将md5加入 客户端操作信息

        self.sock.send(json.dumps(data_header).encode()) #发送客户端的操作信息
        response = self.get_response()  #接收服务端返回的 操作信息
        print(response)

        if response["status_code"] ==257: #服务端返回的状态码是:传输中
            self.sock.send(b'1')  # send confirmation to server
            base_filename = cmd_list[1].split('/')[-1] #取出要接收的文件名
            received_size = 0  #本地接收总量
            file_obj = open(base_filename,'wb') #bytes模式写入

            if self._md5_required(cmd_list): #命令请求里有 --md5
                md5_obj = hashlib.md5()

                progress = self.show_progress(response['file_size'])
                progress.__next__()

                while received_size < response['file_size']: #当接收的量 小于 文件总量 就循环接收文件
                    data = self.sock.recv(4096) #一次接收4096
                    received_size += len(data) #本地接收总量每次递增

                    try:
                        progress.send(len(data))
                    except StopIteration as e:
                        print("100%")

                    file_obj.write(data) #把接收的数据 写入文件
                    md5_obj.update(data) #把接收的数据 md5加密
                else:
                    print("--->file rece done<---") #成功接收文件
                    file_obj.close() #关闭文件句柄
                    md5_val = md5_obj.hexdigest()
                    md5_from_server = self.get_response()  #获取服务端发送的 md5
                    if md5_from_server['status_code'] ==258:  #状态码为258
                        if md5_from_server['md5'] == md5_val:  #两端 md5值 对比
                            print("%s 文件一致性校验成功!" %base_filename)
                    # print(md5_val,md5_from_server)
            else:  #没有md5校验 直接收文件
                progress = self.show_progress(response['file_size'])
                progress.__next__()

                while received_size < response['file_size']: #当接收的量 小于 文件总量 就循环接收文件
                    data = self.sock.recv(4096) #一次接收4096
                    received_size += len(data) #本地接收总量每次递增
                    file_obj.write(data) #把接收的数据 写入文件
                    try:
                        progress.send(len(data))
                    except StopIteration as e:
                        print("100%")
                else:
                    print("--->file rece done<---") #成功接收文件
                    file_obj.close() #关闭文件句柄

    def _put(self,cmd_list):
        ''' put 下载方法'''
        print("put--", cmd_list)
        if len(cmd_list) == 1:
            print("no filename follows...")
            return
        # 客户端操作信息
        data_header = {
            'action': 'put',
            'filename': cmd_list[1],
        }
        self.sock.send(json.dumps(data_header).encode())  # 发送客户端的操作信息
        self.sock.recv(1)
        file_obj = open(cmd_list[1],'br')
        for line in file_obj:
            self.sock.send(line)


if __name__ == '__main__':
    ftp = FTPClient()
    ftp.interactive()

  

 

 

 
posted @ 2018-03-19 20:30  努力哥  阅读(431)  评论(0)    收藏  举报