python 多用戶在線的FTP程序


要求:

1、用戶加密認證

2、允許同時多用戶登錄

3、每個用戶有自己的家目錄 ,且只能訪問自己的家目錄

4、對用戶進行磁盤配額,每個用戶的可用空間不同

5、允許用戶在ftp server上隨意切換目錄

6、允許用戶查看當前目錄下文件

7、允許上傳和下載文件,保證文件一致性

8、文件傳輸過程中顯示進度條

9、附加功能:支持文件的斷點續傳


README:

設計說明

1、client連接server端需要驗證賬號密碼,密碼使用MD5加密傳輸。

2、用戶信息保存在本地文件中,密碼MD5加密存儲。磁盤配額大小也保存在其中。

3、用戶連接上來後,可以執行命令如下

    目錄變更:cd /cd dirname / cd . /cd ..

    文件瀏覽:ls

     文件刪除:rm filename

    目錄增刪:mkdir dirname /rmdir dirname

    查看當前目錄:pwd

    查看當前目錄大小: du

    上傳文件:put filename

    下載文件:get filename

    移動和重命名: mv filename/dirname filename/dirname

     上傳斷點續傳: newput filename

    下載斷點續傳: newget filename

4、涉及到目錄的操作,用戶登錄後,程序會給用戶一個“錨位”----以用戶名字命名的家目錄,使用戶無論怎麼操作,都只能在這個目錄底下。而在發給用戶的目錄信息時,隱去上層目錄信息。

5、用戶在創建時,磁盤配額大小默認是100M,在上傳文件時,程序會計算當前目錄大小加文件大小是否會超過配額上限。未超過,上傳;超過,返回磁盤大小不夠的信息。磁盤配額可通過用戶管理程序修改。

6、文件上傳和下載後都會進行MD5值比對,驗證文件是否一致。

7、服務端和客戶端都有顯示進度條功能,啓用該功能會降低文件傳輸速度,這是好看的代價。

8、文件斷點續傳,支持文件上傳和下載斷點續傳。斷點續傳上傳功能還會檢測用戶控件是否足夠。(斷點續傳命令使用前面new+put/get命名,包含put/get所有功能,由於邏輯增多,代碼複雜,特地保留原put/get,以備後用)。


暫且說到這,接下來是正式程序


wKiom1nNBZ3BkioMAAB6yDPSdzI380.jpg-wh_50


試運行截圖

wKioL1nNBOjwhBEiAAGwpqCk5VA361.jpg-wh_50



代碼如下:

1、服務端

server.conf

####用戶端配置文件####
[DEFAULT]
logfile = ../log/server.log
usermgr_log = ../log/usermgr.log
upload_dir= ../user_files
db_dir = ../db

####日誌文件位置####
[log]
logfile = ../log/server.log
usermgr_log = ../log/usermgr.log

####上傳文件存放位置####
[upload]
upload_dir= ../user_files

####用戶信息存放位置####
[db]
db_dir = ../db


main.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socketserver,os
from usermanagement import useropr
from server import MyTCPHandler

info = '''
        1、啓動服務器
        2、進入用戶管理
        按q退出
'''

if __name__ == '__main__':
    while True:
        print(info)
        choice = input('>>>:')
        if choice == 'q':
            exit()
        elif choice == '1':
            ip, port = '0.0.0.0', 9999
            server = socketserver.ThreadingTCPServer((ip, port), MyTCPHandler)
            server.serve_forever()
        elif choice == '2':
            useropr.interactive()
        else:continue

usermanagement

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#filename:usermanagement.py
import os,hashlib,time,pickle,shutil,configparser,logging


####讀取配置文件####
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_file = os.path.join(base_dir, 'conf/server.conf')
cf = configparser.ConfigParser()
cf.read(config_file)
####設定日誌目錄####
if os.path.exists(cf.get('log','usermgr_log')):
    logfile = cf.get('log', 'usermgr_log')
else:
    logfile = os.path.join(base_dir,'log/usermgr.log')
####設定用戶上傳文件目錄,這邊用於創建用戶家目錄使用####
if os.path.exists(cf.get('upload','upload_dir')):
    file_dir = cf.get('upload','upload_dir')
else:
    file_dir = os.path.join(base_dir,'user_files')
####設定用戶信息存儲位置####
if os.path.exists(cf.get('db','db_dir')):
    db_path = cf.get('db','db_dir')
else:
    db_path = os.path.join(base_dir,'db')


def hashmd5(*args):             ####用於加密密碼信息
    m = hashlib.md5()
    m.update(str(*args).encode())
    return m.hexdigest()


class useropr(object):
    def __init__(self,user_name,passwd = '123456',phone_number=''):
        self.user_name = user_name
        self.id = time.strftime("%Y%m%d%H%M%S", time.localtime())
        self.phone_number = phone_number
        self.passwd = passwd
        self.space_size = 104857600         ####初始分配100MB存儲空間
        self.member_level = 1               ####會員等級,初始爲1,普通會員

    @staticmethod                       ####使用靜態方法,可以直接用類命調用,如user.search_user(username),否則需要實例化一個對象後才能調用
    def query_user(user_name):        ####查詢用戶
        db_filelist=os.listdir(db_path)
        #print(db_filelist)
        dict={}
        for filename in db_filelist:
            with open(os.path.join(db_path,filename),'rb') as f:
                content=pickle.load(f)
                #print(filename,content)    ####開啓會打印出所有用戶信息
                if content['username'] == user_name:
                    #print(filename, content)
                    dict={'filename':filename,'content':content}
                    return dict


    def save_userinfo(self):                    ####保存用戶信息
        query_result = self.query_user(self.user_name)      ####檢查是否已存在同名用戶,如果沒有查詢結果應該爲None
        if query_result == None:
            user_info = {
                'username':self.user_name,
                'id':self.id,
                'phonenumber':self.phone_number,
                'passwd':hashmd5(self.passwd),
                'spacesize':self.space_size,
                'level':self.member_level
            }
            with open(os.path.join(db_path,self.id),'wb') as f:
                pickle.dump(user_info,f)
                print('用戶信息保存完畢')
                try:                                                    ####創建用戶家目錄
                    os.mkdir(os.path.join(file_dir, self.user_name))
                    print('用戶目錄創建成功!')
                except Exception as e:
                    print('用戶目錄創建失敗,',e)

        else:
            print('用戶名重複,信息未保存')

    @staticmethod
    def change_info(user_name,**kwargs):           ####修改信息
        query_result = useropr.query_user(user_name)  ####用於檢測用戶是否存在,不存在不處理
        if query_result != None:
            userinfo_filename = query_result['filename']
            user_info = query_result['content']
            print('before update:',user_info)
            for key in kwargs:
                if key in ('username','id'):            ####用戶名和ID不可更改
                    print(key,'項不可更改')
                elif key in ('passwd','phonenumber','spacesize','level'):         ####允許修改的鍵值
                    if key == 'passwd':
                        user_info[key] = hashmd5(kwargs[key])  ####加密密碼保存
                    else:
                        user_info[key] = kwargs[key]
                    with open(os.path.join(db_path, userinfo_filename), 'wb') as f:
                        pickle.dump(user_info, f)
                        print(key,'項用戶信息變更保存完畢')
                else:
                    print('輸入信息錯誤,',key,'項不存在')
            print('after update:',user_info)
        else:
            print('用戶不存在')

    @staticmethod
    def delete_user(user_name):              ####刪除用戶
        query_result = useropr.query_user(user_name)  ####用於檢測用戶是否存在,不存在不處理
        if query_result != None:
            userinfo_filename = query_result['filename']
            userfile_path=os.path.join(db_path, userinfo_filename)
            os.remove(userfile_path)
            query_result_again = useropr.query_user(user_name)
            if query_result_again == None:
                print('用戶DB文件刪除成功')
                try:
                    shutil.rmtree(os.path.join(file_dir,user_name))
                    print('用戶家目錄刪除成功')
                except Exception as e:
                    print('用戶家目錄刪除失敗:',e)
            else:
                print('用戶DB文件刪除失敗')

        else:
            print('用戶不存在或者已經被刪除')

    @staticmethod
    def query_alluser():        ####查詢所有用戶信息,用於調試使用
        db_filelist=os.listdir(db_path)
        for filename in db_filelist:
            with open(os.path.join(db_path,filename),'rb') as f:
                content=pickle.load(f)
                print(filename,content)

    @staticmethod
    def interactive():
        '''使用說明:
        新增用戶請輸入類似: a=useropr(username,passwd)
                            a.save_userinfo()
        查詢用戶請輸入:useropr.query_user(username)
        更改用戶信息請輸入:useropr.change_info(username,id=123,level=1,passwd=123,phonenumber=123),其中字典部分爲可選項
        用戶刪除請輸入:useropr.delete_user(username)
        '''
        info='''
        1、新增用戶
        2、查詢用戶
        3、修改用戶
        4、刪除用戶
        退出請按q
        '''


        #useropr.query_alluser()        ####查詢所有用戶信息,調試用

        while True:
            print(info)
            choice = input('請輸入你的選擇:').strip()
            #print('operation choice: %s' % choice)
            if choice == 'q':
                exit()
            else:
                username = input('請輸入用戶名:').strip()
                #print('username: %s' % username)
                if username == '':
                    print('用戶不能爲空')
                    continue
                elif choice == '1':
                    passwd = input('請輸入密碼:')
                    new_user = useropr(username, passwd)
                    new_user.save_userinfo()

                elif choice == '2':
                    print(useropr.query_user(username))

                elif choice == '3':
                    update_item = input('請輸入要修改的項目,例如:level,passwd,phonenumber:')
                    print('update item: %s' % update_item)
                    update_value = input('請輸入要修改的項目新值:')
                    useropr.change_info(username,**{update_item:update_value})      #### ‘**{}’ 不加**系統無法識別爲字典。不能直接使用update_item=update_value,update_item會直接被當成key值,而不是其中的變量。

                elif choice == '4':
                    useropr.delete_user(username)

                else:
                    print('輸入錯誤')
                    continue

if __name__ == '__main__':
    useropr.interactive()

server.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# filename:server.py
import socketserver, json, os, sys, time, shutil, configparser, logging
from usermanagement import useropr

####讀取配置文件####
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_file = os.path.join(base_dir, 'conf/server.conf')
cf = configparser.ConfigParser()
cf.read(config_file)
####設定日誌目錄####
if os.path.exists(cf.get('log', 'logfile')):
    logfile = cf.get('log', 'logfile')
else:
    logfile = os.path.join(base_dir, 'log/server.log')
####設定用戶上傳文件目錄####
if os.path.exists(cf.get('upload', 'upload_dir')):
    file_dir = cf.get('upload', 'upload_dir')
else:
    file_dir = os.path.join(base_dir, 'user_files')

####設置日誌格式###
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d    %H:%M:%S',
                    filename=logfile,
                    filemode='a+')


def TimeStampToTime(timestamp):  ####輸入timestamp格式化輸出時間,輸出格式如:2017-09-16 16:32:35
    timeStruct = time.localtime(timestamp)
    return time.strftime('%Y-%m-%d %H:%M:%S', timeStruct)


def ProcessBar(part, total):  ####進度條模塊,運行會導致程序變慢
    if total != 0:
        i = round(part * 100 / total)
        sys.stdout.write(
            '[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r')
        sys.stdout.flush()
        # if part == total:
        #     print()


class MyTCPHandler(socketserver.BaseRequestHandler):

    def put(self, *args):  ####接收客戶端文件
        # self.request.send(b'server have been ready to receive')    ####發送ACK
        cmd_dict = args[0]
        filename = os.path.basename(cmd_dict['filename'])  ####傳輸進來的文件名可能帶有路徑,將路徑去掉
        filesize = cmd_dict['filesize']
        filemd5 = cmd_dict['filemd5']
        override = cmd_dict['override']
        receive_size = 0
        file_path = os.path.join(self.position, filename)
        if override != 'True' and os.path.exists(file_path):  ####檢測文件是否已經存在
            self.request.send(b'file have exits, do nothing!')
        else:
            if os.path.isfile(file_path):  ####如果文件已經存在,先刪除,再計算磁盤空間大小
                os.remove(file_path)
            current_size = self.du()  ####調用du查看用戶磁盤空間大小,但是du命令的最後會發送一個結果信息給client,會和前面和後面的信息粘包,需要注意
            self.request.recv(1024)  ####接收客戶端ack信號,防止粘包,代號:P01
            print(self.user_spacesize, current_size, filesize)
            if self.user_spacesize >= current_size + filesize:
                self.request.send(b'begin')  ####發送開始傳輸信號
                fk = open(file_path, 'wb')
                while filesize > receive_size:
                    if filesize - receive_size > 1024:
                        size = 1024
                    else:
                        size = filesize - receive_size
                    data = self.request.recv(size)
                    fk.write(data)
                    receive_size += len(data)
                    # print(receive_size,len(data))   ####打印每次接收的數據
                    # ProcessBar(receive_size, filesize)  ####服務端進度條,不需要可以註釋掉

                fk.close()
                receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
                print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                if receive_filemd5 == filemd5:
                    self.request.send(b'file received successfully!')
                else:
                    self.request.send(b'Error, file received have problems!')
            else:
                self.request.send(
                    b'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % (
                    self.user_spacesize, current_size, self.user_spacesize - current_size, filesize))

    def get(self, *args):  ####發送給客戶端文件
        # print('get receive the cmd',args[0])
        filename = args[0]['filename']
        print(filename)
        # self.request.send(b'server have been ready to send')  ####發送ACK
        file_path = os.path.join(self.position, filename)
        if os.path.isfile(file_path):
            filesize = os.path.getsize(file_path)
            ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低
            filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
            msg = {
                'action': 'get',
                'filename': filename,
                'filesize': filesize,
                'filemd5': filemd5,
                'override': 'True'
            }
            print(msg)
            self.request.send(json.dumps(msg).encode('utf-8'))

            '''接下來發送文件給客戶端'''
            self.request.recv(1024)  ####接收ACK信號,下一步發送文件
            fk = open(file_path, 'rb')
            send_size = 0
            for line in fk:
                send_size += len(line)
                self.request.send(line)
                # ProcessBar(send_size, filesize)     ####服務端進度條,不需要可以註釋掉
            else:
                print('文件傳輸完畢')
                fk.close()

        else:
            print(file_path, '文件未找到')
            self.request.send(json.dumps('Filenotfound').encode('utf-8'))

    def newput(self, *args):  ####接收客戶端文件,具有斷點續傳功能
        # self.request.send(b'server have been ready to receive')    ####發送ACK
        cmd_dict = args[0]
        filename = os.path.basename(cmd_dict['filename'])  ####傳輸進來的文件名可能帶有路徑,將路徑去掉
        filesize = cmd_dict['filesize']
        filemd5 = cmd_dict['filemd5']
        override = cmd_dict['override']
        receive_size = 0
        file_path = os.path.join(self.position, filename)
        print(file_path,os.path.isdir(file_path))
        if override != 'True' and os.path.exists(file_path):  ####檢測文件是否已經存在
            if os.path.isdir(file_path):
                self.request.send(b'file have exits, and is a directory, do nothing!')
            elif os.path.isfile(file_path):
                self.request.send(b'file have exits, do nothing!')
                resume_signal = self.request.recv(1024)     ####接收客戶端發來的是否從文件斷點續傳的信號
                if resume_signal == b'ready to resume from break point':           ####執行斷點續傳功能
                    exits_file_size = os.path.getsize(file_path)
                    current_size = self.du()
                    time.sleep(0.5) ####防止粘包
                    print('用戶空間上限:%d, 當前已用空間:%d, 已存在文件大小:%d, 上傳文件大小:%d ' % (self.user_spacesize,current_size,exits_file_size,filesize))
                    if self.user_spacesize >= (current_size - exits_file_size + filesize):  ####判斷剩餘空間是否足夠
                        if exits_file_size < filesize:
                            receive_size = exits_file_size
                            print('服務器上已存在的文件大小爲:',exits_file_size)
                            msg = {
                                'state': True,
                                'position': exits_file_size,
                                'content': 'ready to receive file'
                            }
                            self.request.send(json.dumps(msg).encode('utf-8'))
                            fk = open(file_path, 'ab+')
                            while filesize > receive_size:
                                if filesize - receive_size > 1024:
                                    size = 1024
                                else:
                                    size = filesize - receive_size
                                data = self.request.recv(size)
                                fk.write(data)
                                receive_size += len(data)
                                # print(receive_size,len(data))   ####打印每次接收的數據
                                # ProcessBar(receive_size, filesize)  ####服務端進度條,不需要可以註釋掉

                            fk.close()
                            receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
                            print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                            if receive_filemd5 == filemd5:
                                self.request.send(b'file received successfully!')
                            else:
                                self.request.send(b'Error, file received have problems!')

                        else:       ####如果上傳的文件小於當前服務器上的文件,則爲同名但不同文件,不上傳。實際還需要增加其他判斷條件,判斷是否爲同一文件。
                            msg = {
                                'state': False,
                                'position': '',
                                'content': 'Error, file mismatch, do nothing!'
                            }
                            self.request.send(json.dumps(msg).encode('utf-8'))
                    else:       ####如果續傳後的用戶空間大於上限,拒接續傳
                        msg = {
                            'state': False,
                            'position':'',
                            'content':'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, need_size:%d' % (self.user_spacesize, current_size, self.user_spacesize - current_size, filesize - exits_file_size)
                        }
                        self.request.send(json.dumps(msg).encode('utf-8'))
                else:
                    pass

        else:
            if os.path.isfile(file_path):  ####如果文件已經存在,先刪除,再計算磁盤空間大小
                os.remove(file_path)
            current_size = self.du()  ####調用du查看用戶磁盤空間大小,但是du命令的最後會發送一個結果信息給client,會和前面和後面的信息粘包,需要注意
            self.request.recv(1024)  ####接收客戶端ack信號,防止粘包,代號:P01
            print(self.user_spacesize, current_size, filesize)
            if self.user_spacesize >= current_size + filesize:
                self.request.send(b'begin')  ####發送開始傳輸信號
                fk = open(file_path, 'wb')
                while filesize > receive_size:
                    if filesize - receive_size > 1024:
                        size = 1024
                    else:
                        size = filesize - receive_size
                    data = self.request.recv(size)
                    fk.write(data)
                    receive_size += len(data)
                    # print(receive_size,len(data))   ####打印每次接收的數據
                    # ProcessBar(receive_size, filesize)  ####服務端進度條,不需要可以註釋掉

                fk.close()
                receive_filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
                print('\r\n', file_path, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                if receive_filemd5 == filemd5:
                    self.request.send(b'file received successfully!')
                else:
                    self.request.send(b'Error, file received have problems!')
            else:
                self.request.send(
                    b'Error, disk space do not enough! Nothing done! Total: %d, current: %d, rest:%d, filesize:%d' % (
                    self.user_spacesize, current_size, self.user_spacesize - current_size, filesize))

    def newget(self, *args):  ####發送給客戶端文件,具有斷點續傳功能
        # print('get receive the cmd',args[0])
        filename = args[0]['filename']
        remote_local_filesize = args[0]['filesize']
        print(filename)
        # self.request.send(b'server have been ready to send')  ####發送ACK
        file_path = os.path.join(self.position, filename)
        if os.path.isfile(file_path):
            filesize = os.path.getsize(file_path)
            ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低
            filemd5 = os.popen('md5sum %s' % file_path).read().split()[0]
            msg = {
                'action': 'newget',
                'filename': filename,
                'filesize': filesize,
                'filemd5': filemd5,
                'override': 'True'
            }
            print(msg)
            self.request.send(json.dumps(msg).encode('utf-8'))

            '''接下來發送文件給客戶端'''
            self.request.recv(1024)  ####接收ACK信號,下一步發送文件
            fk = open(file_path, 'rb')
            fk.seek(remote_local_filesize,0)
            send_size = remote_local_filesize
            for line in fk:
                send_size += len(line)
                self.request.send(line)
                # ProcessBar(send_size, filesize)     ####服務端進度條,不需要可以註釋掉
            else:
                print('文件傳輸完畢')
                fk.close()

        else:
            print(file_path, '文件未找到')
            self.request.send(json.dumps('Filenotfound').encode('utf-8'))

    def pwd(self, *args):
        current_position = self.position
        result = current_position.replace(file_dir, '')  ####截斷目錄信息,使用戶只能看到自己的家目錄信息
        self.request.send(json.dumps(result).encode('utf-8'))

    def ls(self, *args):  ####列出當前目錄下的所有文件信息,類型,字節數,生成時間。
        result = ['%-20s%-7s%-10s%-23s' % ('filename', 'type', 'bytes', 'creationtime')]  ####信息標題
        for f in os.listdir(self.position):
            type = 'unknown'
            f_abspath = os.path.join(self.position, f)  ####給出文件的絕對路徑,不然程序會找不到文件
            if os.path.isdir(f_abspath):
                type = 'd'
            elif os.path.isfile(f_abspath):
                type = 'f'
            result.append('%-20s%-7s%-10s%-23s' % (
            f, type, os.path.getsize(f_abspath), TimeStampToTime(os.path.getctime(f_abspath))))
        self.request.send(json.dumps(result).encode('utf-8'))

    def du(self, *args):
        '''統計純文件和目錄佔用空間大小,結果小於在OS上使用du -s查詢,因爲有一些(例如'.','..')隱藏文件未包含在內'''
        totalsize = 0
        if os.path.isdir(self.position):
            dirsize, filesize = 0, 0
            for root, dirs, files in os.walk(self.position):
                for d_item in dirs:  ####計算目錄佔用空間,Linux中每個目錄佔用4096bytes,實際上也可以按這個值來相加
                    if d_item != '':
                        dirsize += os.path.getsize(os.path.join(root, d_item))
                for f_item in files:  ####計算文件佔用空間
                    if f_item != '':
                        filesize += os.path.getsize(os.path.join(root, f_item))
            totalsize = dirsize + filesize
            result = 'current directory total sizes: %d' % totalsize
        else:
            result = 'Error,%s is not path ,or path does not exist!' % self.position
        self.request.send(json.dumps(result).encode('utf-8'))
        return totalsize

    def cd(self, *args):
        print(*args)
        user_homedir = os.path.join(file_dir, self.username)
        cmd_dict = args[0]
        error_tag = False
        '''判斷目錄信息'''
        if cmd_dict['dir'] == '':
            self.position = user_homedir
        elif cmd_dict['dir'] == '.' or cmd_dict['dir'] == '/' or '//' in cmd_dict['dir']:  ####'.','/','//','///+'匹配
            pass
        elif cmd_dict['dir'] == '..':
            if user_homedir != self.position and user_homedir in self.position:  ####當前目錄不是家目錄,並且當前目錄是家目錄下的子目錄
                self.position = os.path.dirname(self.position)
        elif '.' not in cmd_dict['dir'] and os.path.isdir(
                os.path.join(self.position, cmd_dict['dir'])):  ####'.' not in cmd_dict['dir'] 防止../..輸入
            self.position = os.path.join(self.position, cmd_dict['dir'])
        else:
            error_tag = True
        '''發送結果'''
        if error_tag:
            result = 'Error,%s is not path here, or path does not exist!' % cmd_dict['dir']
            self.request.send(json.dumps(result).encode('utf-8'))
        else:
            self.pwd()

    def mv(self,*args):
        print(*args)
        try:
            objectname = args[0]['objectname']
            dstname = args[0]['dstname']
            abs_objectname = os.path.join(self.position,objectname)
            abs_dstname = os.path.join(self.position, dstname)
            print(abs_objectname,abs_dstname,os.path.isfile(abs_objectname),os.path.isdir(abs_objectname),os.path.isdir(abs_dstname))
            result = ''
            if os.path.isfile(abs_objectname):
                if os.path.isdir(abs_dstname) or not os.path.exists(abs_dstname):
                    shutil.move(abs_objectname, abs_dstname)
                    print('moving success')
                    result = 'moving success'

                elif os.path.isfile(abs_dstname):
                    print('moving cancel, file has been exits')
                    result = 'moving cancel, file has been exits'

            elif os.path.isdir(abs_objectname):
                if os.path.isdir(abs_dstname) or not os.path.exists(abs_dstname):
                    shutil.move(abs_objectname, abs_dstname)
                    print('moving success')
                    result = 'moving success'

                elif os.path.isfile(abs_dstname):
                    print('moving cancel, %s is file' % dstname)
                    result = 'moving cancel, %s is file' % dstname

            else:
                print('nothing done')
                result = 'nothing done'
            self.request.send(json.dumps(result).encode('utf-8'))

        except Exception as e:
            print(e)
            result = 'moving fail,' + e
            self.request.send(json.dumps(result).encode('utf-8'))

    def mkdir(self, *args):  ####創建目錄
        try:
            dirname = args[0]['dirname']
            if dirname.isalnum():  ####判斷文件是否只有數字和字母
                if os.path.exists(os.path.join(self.position, dirname)):
                    result = '%s have existed' % dirname
                else:
                    os.mkdir(os.path.join(self.position, dirname))
                    result = '%s created succes' % dirname
            else:
                result = 'Illegal character %s, dirname can only by string and num here.' % dirname
        except TypeError:
            result = 'please input dirname'
        self.request.send(json.dumps(result).encode('utf-8'))

    def rm(self, *args):  ####刪除文件
        filename = args[0]['filename']
        confirm = args[0]['confirm']
        file_abspath = os.path.join(self.position, filename)
        if os.path.isfile(file_abspath):
            if confirm == True:
                os.remove(file_abspath)
                result = '%s have been delete.' % filename
            else:
                result = 'Not file deleted'
        elif os.path.isdir(file_abspath):
            result = '%s is a dir, plsese using rmdir' % filename
        else:
            result = 'File %s not exist!' % filename
        self.request.send(json.dumps(result).encode('utf-8'))

    def rmdir(self, *args):  ###刪除目錄
        dirname = args[0]['dirname']
        confirm = args[0]['confirm']
        file_abspath = os.path.join(self.position, dirname)
        if '.' in dirname or '/' in dirname:  ####不能跨目錄刪除
            result = 'should not rmdir %s this way' % dirname
        elif os.path.isdir(file_abspath):
            if confirm == True:
                shutil.rmtree(file_abspath)
                result = '%s have been delete.' % dirname
            else:
                result = 'Not file deleted'
        elif os.path.isfile(file_abspath):
            result = '%s is a file, not directory deleted' % dirname
        else:
            result = 'directory %s not exist!' % dirname
        self.request.send(json.dumps(result).encode('utf-8'))

    def auth(self):
        self.data = json.loads(self.request.recv(1024).decode('utf-8'))
        print(self.data)
        recv_username = self.data['username']
        recv_passwd = self.data['passwd']
        query_result = useropr.query_user(recv_username)
        print(query_result)
        if query_result == None:
            self.request.send(b'user does not exits')
        elif query_result['content']['passwd'] == recv_passwd:
            self.request.send(b'ok')
            return query_result  ####返回查詢結果
        elif query_result['content']['passwd'] != recv_passwd:
            self.request.send(b'password error')
        else:
            self.request.send(b'unknown error')

    def handle(self):  ####處理類,調用以上方法
        # self.position = file_dir
        # print(self.position)
        auth_tag = False
        while auth_tag != True:
            auth_result = self.auth()  ####用戶認證,如果通過,返回用戶名,不通過爲None
            print('the authentication result is:', auth_result)
            if auth_result != None:
                self.username = auth_result['content']['username']
                self.user_spacesize = auth_result['content']['spacesize']
                auth_tag = True
        print(self.username, self.user_spacesize)
        user_homedir = os.path.join(file_dir, self.username)
        if os.path.isdir(user_homedir):
            self.position = user_homedir  ####定錨,用戶家目錄
            print(self.position)
            while True:
                print('當前連接:', self.client_address)
                self.data = self.request.recv(1024).strip()
                print(self.data)
                logging.info(self.client_address)
                if len(self.data) == 0:
                    print('客戶端斷開連接')
                    break  ####檢查發送來的命令是否爲空
                cmd_dict = json.loads(self.data.decode('utf-8'))
                action = cmd_dict['action']
                logging.info(cmd_dict)
                if hasattr(self, action):
                    func = getattr(self, action)
                    func(cmd_dict)
                else:
                    print('未支持指令:', action)
                logging.info('current directory:%s' % self.position)


if __name__ == '__main__':
    ip, port = '0.0.0.0', 9999
    server = socketserver.ThreadingTCPServer((ip, port), MyTCPHandler)
    server.serve_forever()




2、客戶端

client.conf

####用戶端配置文件####
[DEFAULT]
logfile = ../log/client.log
download_dir= ../temp

####日誌文件位置####
[log]
logfile = ../log/client.log

####下載文件存放位置####
[download]
download_dir= ../temp


main.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import configparser,os
from client import FtpClient


if __name__ == '__main__':
    ftp = FtpClient()
    ftp.connect('127.0.0.1',9999)
    auth_tag=False
    while auth_tag != True:
        auth_tag=ftp.auth()
    ftp.interactive()


client.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# filename:client.py
import socket, json, os, sys, hashlib, getpass, logging, configparser,time

####讀取配置文件####
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_file = os.path.join(base_dir, 'conf/client.conf')
cf = configparser.ConfigParser()
cf.read(config_file)
####設定日誌目錄####
if os.path.exists(cf.get('log', 'logfile')):
    logfile = cf.get('log', 'logfile')
else:
    logfile = os.path.join(base_dir, 'log/client.log')
####設定下載目錄####
if os.path.exists(cf.get('download', 'download_dir')):
    download_dir = cf.get('download', 'download_dir')
else:
    download_dir = os.path.join(base_dir, 'temp')

####設置日誌格式###
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename=logfile,
                    filemode='a+')


def hashmd5(*args):  ####用於加密密碼信息
    m = hashlib.md5()
    m.update(str(*args).encode())
    return m.hexdigest()


def ProcessBar(part, total):  ####進度條模塊
    if total != 0:
        i = round(part * 100 / total)
        sys.stdout.write(
            '[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r')
        sys.stdout.flush()


class FtpClient(object):
    def __init__(self):
        self.client = socket.socket()

    def connect(self, ip, port):
        self.client.connect((ip, port))

    def exec_linux_cmd(self, dict):  ####用於後面調用linux命令
        logging.info(dict)  ####將發送給服務端的命令保存到日誌中
        self.client.send(json.dumps(dict).encode('utf-8'))
        server_response = json.loads(self.client.recv(4096).decode('utf-8'))
        if isinstance(server_response, list):
            for i in server_response:
                print(i)
        else:
            print(server_response)

    def help(self):
        info = '''
        僅支持如下命令:
        ls
        du
        pwd
        cd dirname/cd ./cd ..
        mkdir dirname
        rm  filename
        rmdir dirname
        put filename
        get filename
        mv filename/dirname filename/dirname
        newput filename (後續增加的新功能,支持斷點續傳)
        newget filename (後續增加的新功能,支持斷點續傳)
        '''
        print(info)

    def interactive(self):
        while True:
            self.pwd()  ####打印當前目錄位置
            cmd = input('>>>:').strip()
            if len(cmd) == 0: continue
            action = cmd.split()[0]
            if hasattr(self, action):
                func = getattr(self, action)
                func(cmd)
            else:
                self.help()

    def put(self, *args):  ####上傳文件
        cmd = args[0].split()
        override = cmd[-1]  ####override:是否覆蓋參數,放在最後一位
        if override != 'True':
            override = 'False'
        # print(cmd,override)
        if len(cmd) > 1:
            filename = cmd[1]
            if os.path.isfile(filename):
                filesize = os.path.getsize(filename)
                filemd5 = os.popen('md5sum %s' % filename).read().split()[
                    0]  ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低
                msg = {
                    'action': 'put',
                    'filename': filename,
                    'filesize': filesize,
                    'filemd5': filemd5,
                    'override': override  ####True ,or False
                }
                logging.info(msg)
                self.client.send(json.dumps(msg).encode('utf-8'))
                server_response = self.client.recv(1024)  ####等待服務器確認信號,防止粘包
                logging.info(server_response)
                if server_response == b'file have exits, do nothing!':
                    override_tag = input('文件已存在,要覆蓋文件請輸入yes >>>:')
                    if override_tag == 'yes':
                        self.put('put %s True' % filename)
                    else:
                        print('文件未上傳')

                else:
                    self.client.send(b'client have ready to send')  ####發送確認信號,防止粘包,代號:P01
                    server_response = self.client.recv(1024).decode('utf-8')
                    print(server_response)  ####注意:用於打印服務器反饋信息,例如磁盤空間不足信息,不能取消
                    if server_response == 'begin':
                        fk = open(filename, 'rb')
                        send_size = 0
                        for line in fk:
                            # print(len(line))
                            send_size += len(line)
                            self.client.send(line)
                            ProcessBar(send_size, filesize)
                        else:
                            print('\r\n', '文件傳輸完畢')
                            fk.close()
                            server_response = self.client.recv(1024).decode('utf-8')
                            print(server_response)

            else:
                print('文件不存在')
        else:
            print('請輸入文件名')

    def get(self, *args):  ####下載文件
        cmd = args[0].split()
        # print(args[0],cmd)
        if len(cmd) > 1:
            filename = cmd[1]
            filepath = os.path.join(download_dir, filename)
            if os.path.isfile(filepath):           ####判斷下載目錄是否已存在同名文件
                override_tag = input('文件已存在,要覆蓋文件請輸入yes >>>:').strip()
                if override_tag == 'yes':
                    msg = {
                        'action': 'get',
                        'filename': filename,
                        'filesize': 0,
                        'filemd5': '',
                        'override': 'True'
                    }
                    logging.info(msg)
                    self.client.send(json.dumps(msg).encode('utf-8'))
                    server_response = json.loads(self.client.recv(1024).decode('utf-8'))
                    logging.info(server_response)
                    if server_response == 'Filenotfound':
                        print('File no found!')
                    else:
                        print(server_response)
                        self.client.send(b'client have been ready to receive')  ####發送信號,防止粘包
                        filesize = server_response['filesize']
                        filemd5 = server_response['filemd5']
                        receive_size = 0
                        fk = open(filepath, 'wb')
                        while filesize > receive_size:
                            if filesize - receive_size > 1024:
                                size = 1024
                            else:
                                size = filesize - receive_size
                            data = self.client.recv(size)
                            fk.write(data)
                            receive_size += len(data)
                            # print(receive_size, len(data))          ####打印數據流情況
                            ProcessBar(receive_size, filesize)  ####打印進度條

                        fk.close()
                        receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                        print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                        if receive_filemd5 == filemd5:
                            print('文件接收完成!')
                        else:
                            print('Error,文件接收異常!')
                else:
                    print('下載取消')
        else:
            print('請輸入文件名')

    def newput(self, *args):  ####上傳文件,具有斷點續傳功能
        cmd = args[0].split()
        override = cmd[-1]  ####override:是否覆蓋參數,放在最後一位
        if override != 'True':
            override = 'False'
        # print(cmd,override)
        if len(cmd) > 1:
            filename = cmd[1]
            if os.path.isfile(filename):
                filesize = os.path.getsize(filename)
                filemd5 = os.popen('md5sum %s' % filename).read().split()[
                    0]  ####直接調用系統命令取得MD5值,如果使用hashlib,需要寫open打開文件-》read讀取文件(可能文件大會很耗時)-》m.update計算三部,代碼量更多,效率也低
                msg = {
                    'action': 'newput',
                    'filename': filename,
                    'filesize': filesize,
                    'filemd5': filemd5,
                    'override': override  ####True ,or False
                }
                logging.info(msg)
                self.client.send(json.dumps(msg).encode('utf-8'))
                server_response = self.client.recv(1024)  ####等待服務器確認信號,防止粘包
                logging.info(server_response)
                print(server_response)
                if server_response == b'file have exits, and is a directory, do nothing!':
                    print('文件已存在且爲目錄,請先修改文件或目錄名字,然後再上傳')
                elif server_response == b'file have exits, do nothing!':
                    override_tag = input('文件已存在,要覆蓋文件請輸入yes,要斷點續傳請輸入r >>>:').strip()
                    if override_tag == 'yes':
                        self.client.send(b'no need to do anything')     ####服務端在等待是否續傳的信號,發送給服務端確認(功能號:s1)
                        time.sleep(0.5)   ####防止黏貼
                        self.put('put %s True' % filename)
                    elif override_tag == 'r':
                        self.client.send(b'ready to resume from break point')       ####服務端在等待是否續傳的信號,發送給服務端確認(功能號:s1)
                        self.client.recv(1024) ####這邊接收服務端發送過來的du信息,不顯示,直接丟棄
                        server_response = json.loads((self.client.recv(1024)).decode())
                        print(server_response)
                        if server_response['state'] == True:
                                exits_file_size = server_response['position']
                                fk = open(filename, 'rb')
                                fk.seek(exits_file_size,0)
                                send_size = exits_file_size
                                for line in fk:
                                    # print(len(line))
                                    send_size += len(line)
                                    self.client.send(line)
                                    ProcessBar(send_size, filesize)
                                else:
                                    print('\r\n', '文件傳輸完畢')
                                    fk.close()
                                    server_response = self.client.recv(1024).decode('utf-8')
                                    print(server_response)
                        else:
                            print(server_response['content'])

                    else:
                        self.client.send(b'no need to do anything')         ####服務端在等待是否續傳的信號,發送給服務端確認(功能號:s1)
                        print('文件未上傳')

                else:
                    self.client.send(b'client have ready to send')  ####發送確認信號,防止粘包,代號:P01
                    server_response = self.client.recv(1024).decode('utf-8')
                    print(server_response)  ####注意:用於打印服務器反饋信息,例如磁盤空間不足信息,不能取消
                    if server_response == 'begin':
                        fk = open(filename, 'rb')
                        send_size = 0
                        for line in fk:
                            # print(len(line))
                            send_size += len(line)
                            self.client.send(line)
                            ProcessBar(send_size, filesize)
                        else:
                            print('\r\n', '文件傳輸完畢')
                            fk.close()
                            server_response = self.client.recv(1024).decode('utf-8')
                            print(server_response)

            else:
                print('文件不存在')
        else:
            print('請輸入文件名')

    def newget(self, *args):  ####下載文件,具有斷點續傳功能
        cmd = args[0].split()
        # print(args[0],cmd)
        if len(cmd) > 1:
            filename = cmd[1]
            filepath = os.path.join(download_dir, filename)
            transfer_tag = True         ####傳輸控制信號,默認True爲下載
            resume_tag = False          ####斷點續傳信號
            local_filesize = 0          ####本地文件大小,後面判斷是否有同名文件使用
            if os.path.isfile(filepath):           ####判斷下載目錄是否已存在同名文件
                override_tag = input('文件已存在,要覆蓋文件請輸入yes,要斷點續傳請輸入r >>>:').strip()
                if override_tag == 'yes':
                    pass
                elif override_tag == 'r':
                    local_filesize = os.path.getsize(filepath)
                    resume_tag = True
                else:
                    print('下載取消')
                    transfer_tag = False

            if transfer_tag == True:
                msg = {
                    'action': 'newget',
                    'filename': filename,
                    'filesize': local_filesize,
                    'filemd5': '',
                    'override': 'True'
                }
                logging.info(msg)
                self.client.send(json.dumps(msg).encode('utf-8'))
                server_response = json.loads(self.client.recv(1024).decode('utf-8'))
                logging.info(server_response)
                if server_response == 'Filenotfound':
                    print('File no found!')
                else:
                    print(server_response)
                    self.client.send(b'client have been ready to receive')  ####發送信號,防止粘包
                    filesize = server_response['filesize']
                    filemd5 = server_response['filemd5']
                    receive_size = local_filesize
                    if resume_tag == True:
                        fk = open(filepath, 'ab+')      ####用於斷點續傳
                    else:
                        fk = open(filepath, 'wb+')      ####用於覆蓋或者新生成文件
                    while filesize > receive_size:
                        if filesize - receive_size > 1024:
                            size = 1024
                        else:
                            size = filesize - receive_size
                        data = self.client.recv(size)
                        fk.write(data)
                        receive_size += len(data)
                        # print(receive_size, len(data))          ####打印數據流情況
                        ProcessBar(receive_size, filesize)  ####打印進度條

                    fk.close()
                    receive_filemd5 = os.popen('md5sum %s' % filepath).read().split()[0]
                    print('\r\n', filename, 'md5:', receive_filemd5, '原文件md5:', filemd5)
                    if receive_filemd5 == filemd5:
                        print('文件接收完成!')
                    else:
                        print('Error,文件接收異常!')
        else:
            print('請輸入文件名')

    def pwd(self, *args):  ####查看用戶目錄
        msg = {
            'action': 'pwd',
        }
        self.exec_linux_cmd(msg)

    def ls(self, *args):  ####查看文件信息
        msg = {
            'action': 'ls',
        }
        self.exec_linux_cmd(msg)

    def du(self, *args):  ####查看當前目錄大小
        msg = {
            'action': 'du',
        }
        self.exec_linux_cmd(msg)

    def cd(self, *args):  ####切換目錄
        try:  ####如果是直接輸入cd,dirname=''
            dirname = args[0].split()[1]
        except IndexError:
            dirname = ''
        msg = {
            'action': 'cd',
            'dir': dirname
        }
        self.exec_linux_cmd(msg)

    def mkdir(self, *args):  ####生成目錄
        try:  ####如果是直接輸入rm,跳出
            dirname = args[0].split()[1]
            msg = {
                'action': 'mkdir',
                'dirname': dirname,
            }
            self.exec_linux_cmd(msg)
        except IndexError:
            print('Not dirname input, do nothing.')
            pass

    def rm(self, *args):  ####刪除文件
        try:  ####如果是直接輸入rm,跳出
            filename = args[0].split()[1]
            msg = {
                'action': 'rm',
                'filename': filename,
                'confirm': True  ####確認是否直接刪除標誌
            }
            self.exec_linux_cmd(msg)
        except IndexError:
            print('Not filename input, do nothing.')
            pass

    def rmdir(self, *args):
        try:  ####如果是直接輸入rm,跳出
            dirname = args[0].split()[1]
            msg = {
                'action': 'rmdir',
                'dirname': dirname,
                'confirm': True  ####確認是否直接刪除標誌
            }
            self.exec_linux_cmd(msg)
        except IndexError:
            print('Not dirname input, do nothing.')
            pass

    def mv(self,*args): ####實現功能:移動文件,移動目錄,文件重命名,目錄重命名
        try:
            objectname = args[0].split()[1]
            dstname = args[0].split()[2]
            msg = {
                'action': 'mv',
                'objectname': objectname,
                'dstname': dstname
            }
            print(msg)
            self.exec_linux_cmd(msg)
        except Exception as e:
            print(e)
            pass

    def auth(self):
        user_name = input('請輸入用戶名>>>:').strip()
        passwd = getpass.getpass('請輸入密碼>>>:').strip()  ####在linux上輸入密碼不顯示
        msg = {
            'username': user_name,
            'passwd': hashmd5(passwd)
        }
        self.client.send(json.dumps(msg).encode('utf-8'))
        server_response = self.client.recv(1024).decode('utf-8')
        if server_response == 'ok':
            print('認證通過!')
            return True
        else:
            print(server_response)
            return False


if __name__ == '__main__':
    ftp = FtpClient()
    ftp.connect('127.0.0.1', 9999)
    auth_tag = False
    while auth_tag != True:
        auth_tag = ftp.auth()
    ftp.interactive()



注:配置文件中的中文註釋,可能會使程序在啓動時報出ASCII decode error,可以去掉。

  另外服務端最好在Linux下啓動,我在windows下啓動日誌輸出模塊會報錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章