基於線程開發一個FTP服務器

一,項目題目:基於線程開發一個FTP服務器

二,項目要求:

基本要求:

1.用戶加密認證
 
2.容許同時多用戶登陸
 
3.每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄
 
4.對用戶進行磁盤配額,每一個用戶的可用空間不一樣
 
5.容許用戶在ftp server上隨意切換目錄
 
6.容許用戶查看當前目錄下文件
 
7.容許上傳和下載文件,保證文件一致性(md5)
 
8.文件傳輸過程當中顯示進度條
 
9.附加功能:支持文件的斷點續傳

  

擴展需求:

做業需求:
 
1.在以前開發的FTP基礎上,開發支持多併發的功能
 
2.不能使用SocketServer模塊,必須本身實現多線程
 
3.必須用到隊列Queue模塊,實現線程池
 
4.容許配置最大併發數,好比容許只有10個併發用戶

 

 

三,注意事項: 

基本操做:知足需求1,2,3,4 
大神要求:代碼寫的健壯、清晰

  

四,項目分析:

1,用戶加密認證

  這個確定須要用到configparser 和hashlib模塊,用md5進行加密,服務端與用戶端
進行交互前,確定須要進行認證,在服務端進行認證,客戶端須要發送用戶名及密碼,但
是爲了安全起見,服務端數據庫中的密碼應該是加密後的密文,客戶端登錄認證時也應該
發送密文到服務端,服務端接受到密文與數據庫中對應的密文進行比較。

  

2,查看本身的當前目錄下的文件

  這個只須要寫一個dir就ok
簡單的說,使用configparse模塊就能夠完成

  

3,文件傳輸中顯示進度條

  下載的進度條比較好實現,咱們能夠從服務端受到將要下載的文件的大小,

  上傳的進度條,咱們能夠利用文件操做的tell()方法,獲取當前指針位置(字節)

4,在以前的基礎上實現多併發的功能

    併發,是僞並行,,即看起來是同時運行的,單個CPU+多道技術就能夠實現併發

    多道技術概念回顧:內存中同時存入多道(多個)程序,cpu從一個進程快速切換
到另一個,使每一個進程各自運行幾十或幾百毫秒,這樣,雖然在某一個瞬間,一個cpu
只能執行一個任務,但在1秒內,cpu卻能夠運行多個進程,這就給人產生了並行的錯覺,
即僞併發,以此來區分多處理器操做系統的真正硬件並行(多個cpu共享同一個物理內存)

  

5,不能使用SocketServer模塊,必須本身實現多線程 

        多線程(即多個控制線程)的概念是,在一個進程中存在多個線程,多個線程共享該進程的地址空間,
至關於一個車間內有多個流水線,都共有一個車間的資源。例如,北京地鐵與上海地鐵是不一樣的進程,而北京
地鐵裏的1號線是一個線程,北京地鐵全部的線路共享該地鐵的全部資源。
    
    爲何使用多線程?
    1,同一個進程內能夠共享該進程內的地址資源
    
    2,建立線程的開銷遠小於建立進程的開銷(建立一個進程,就是建立一個車間,涉及到申請空間
,並且在該空間內至少建立一個流水線,可是建立線程,就只是在一個車間內造一條流水線,無需申請
空間,因此建立開銷小)

  

6,必須用到隊列Queue模塊,實現線程池

  使用線程池便可,若是不會能夠參考此博客的練習題:http://www.cnblogs.com/wj-1314/p/9039970.htmlhtml

 7,容許配置最大併發數,好比容許只有10個併發用戶

8,小編的主要思路

- 1 對於此項目,最初的想法是寫出上傳,和下載文件的程序,包括客戶端和服務端。

-  2 在此基礎上擴展程序,包括提出開始程序到bin裏面,配置文件在config裏面

-  3 而後把上傳文件和下載文件的程序進行斷點續傳的程序重構

-   4 在此基礎上,對文件進行加密

-   5 增長功能,包括設置進度條,增長查看功能,增長目錄功能,刪除文件功能,切換目錄功能等

-   6 而後再設置磁盤分配功能,完善內容

-   7 而後添加用戶登錄,包括對用戶的密碼加密等功能

-   8 寫完後檢查程序

 

五,README文件

 

## 做者:zhanzhengrecheng
## 版本:示例版本 v0.1
## 程序介紹:
- 實現了基於線程開發一個FTP服務器的經常使用功能
- 基本功能所有用python的基礎知識實現,用到了socket\hashlib\configparse\os\sys\pickle\函數\模塊\類知識
- 在保證了支持多併發的功能上,不使用SocketServer模塊,本身實現了多線程,並且使用了隊列

## 概述
本次做業文件夾一共包含了如下4個文件:
- 程序結構圖:整個Thread_based_FTP_homework的程序文件結構
- 程序結構文件:整個Thread_based_FTP_homework的程序文件結構
- 程序文件: Thread_based_FTP_homework
- 程序說明文件:README.md

## 程序要求
- 1.用戶加密認證
- 2.容許同時多用戶登陸
- 3.每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄
- 4.對用戶進行磁盤配額,每一個用戶的可用空間不一樣
- 5.容許用戶在ftp server上隨意切換目錄
- 6.容許用戶查看當前目錄下文件
- 7.容許上傳和下載文件,保證文件一致性(md5)
- 8.文件傳輸過程當中顯示進度條
- 9.附加功能:支持文件的斷點續傳

- 10.在以前開發的FTP基礎上,開發支持多併發的功能 
- 11.不能使用SocketServer模塊,必須本身實現多線程 
- 12.必須用到隊列Queue模塊,實現線程池 
- 13.容許配置最大併發數,好比容許只有10個併發用戶

## 本項目思路
- 1 對於這次項目,在上次做業的基礎上完成
- 2 本次首要任務,爲了下降程序的耦合性,將把server端和client端的許多東西分出來,保證一個函數只作一件事情
- 3 發現了上次做業裏面出現的小問題,進行了解決
- 4 使用隊列Queue模塊,實現多線程
- 5 設置配置最大的併發數,此處設置在settings裏面,最大併發用戶設置爲3


##### 備註(程序結構)
> 目前還不會把程序樹放在README.md裏面,因此作出程序結構的txt版本和圖片版本,放在文件外面方便查看

## 對幾個實例文件的說明
### 幾個實例文件全是爲了上傳和下載使用,本身隨便找的素材,沒有把視頻,照片上傳

## 不足及其改進的方面
### 每次程序從用戶登錄到使用只能完成一次功能,不能重複使用

## 程序結構


│  Thread_based_FTP_homework
│  __init__.py
│  
├─client                # 客戶端程序入口
│  │  __init__.py
│  ├─bin                # 可執行程序入口目錄
│  │      run.py
│  │      __init__.py
│  ├─config             # 配置文件目錄
│  │  │  settings.py    # 配置文件
│  │  │  __init__.py       
│  ├─core                          # 主要邏輯程序目錄
│  │  │  file_func.py              # client端文件操做功能模塊
│  │  │  ftp_client.py             # client端主程序模塊
│  │  │  md5_func.py               # client端對文件加密操做功能模塊
│  │  │  progress_bar_func_func.py # client端文件下載進度條操做功能模塊
│  │  │  __init__.py       
│  ├─download           # 下載內容模塊
│  │      a.txt
│  │      b.txt
│  │      c.txt  
│  └─upload             # 上傳內容模塊
│          a.txt
│          b.txt
└─server                 # 服務端程序入口
    ├─bin
    │      run.py        # 可執行程序入口目錄
    │      __init__.py 
    ├─config             # 配置文件目錄
    │  │  accounts.ini   # 帳號密碼配置文件
    │  │  settings.py    # 配置文件
    │  │  __init__.py        
    ├─core               # 主要邏輯程序目錄
    │  │  ftp_server.py  # server端主程序模塊
    │  │  main.py        # 主程序模塊
    │  │  user_handle.py # 用戶註冊登陸模塊  
    │  │  file_func.py   # server端文件操做功能模塊
    │  │  auth_func.py   # server端用戶認證功能模塊
    │  │  md5_func.py    # server端對文件加密操做功能模塊
    └─home               # 家目錄
        │  __init__.py
        ├─curry          # curry用戶的家目錄
        │  │  a.txt
        │  │  b.txt
        │  │  c.txt
        ├─durant         # durant用戶的家目錄
        │  └─test3
        │  └─test4
        └─james           # james用戶的家目錄
            │  a.txt
            │  b.txt
            │  c.txt
            │  test1
            │  test2
            └─test3

  

六,程序結構圖

 

 

 八,程序代碼

1,server

1.1 bin

run.pypython

# _*_ 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 ftp_server
from core import main
from config import settings

if __name__ == '__main__':
    a = main.Manager()
    a.interactive()

  

1.2config

settings.pygit

# _*_ coding: utf-8 _*_ 
import os
import sys
import socket
import logging
 
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
 
ACCOUNTS_FILE = os.path.join(BASE_DIR,'config','accounts.ini')
 
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
 
BIND_HOST = '127.0.0.1'
BIND_PORT = 9999
ip_port = (BIND_HOST,BIND_PORT)
coding = 'utf-8'
listen_count = 5
max_recv_bytes = 8192
allow_reuser_address = False
# 容許配置最大併發數,這裏設置爲3
MAX_CONCURRENT_COUNT =3

  

1.3core

auth.pyshell

# _*_ coding: utf-8 _*_
import pickle
import hashlib
import os
import struct

from config import settings
from core.user_handle import UserHandle
from core.file_func import File_func

class Auth_func():

    def auth(self, conn):
        '''
        處理用戶的認證請求
        1,根據username讀取accounts.ini文件,而後查看用戶是否存在
        2,將程序運行的目錄從bin.user_auth修改到用戶home/username方便以後查詢
        3,把客戶端返回用戶的詳細信息
        :return:
        '''
        while True:
            user_dic = self.get_recv(conn)
            username = user_dic['username']
            password = user_dic['password']
            md5_obj = hashlib.md5(password.encode('utf-8'))
            check_password = md5_obj.hexdigest()
            user_handle = UserHandle(username)
            # 判斷用戶是否存在 返回列表,
            user_data = user_handle.judge_user()
            if user_data:
                if user_data[0][1] == check_password:
                    conn.send(struct.pack('i', 1))  # 登陸成功返回 1
                    self.homedir_path = os.path.join(settings.BASE_DIR, 'home', username)
                    # 將程序運行的目錄名修改到 用戶home目錄下
                    os.chdir(self.homedir_path)
                    # 將用戶配額的大小從M 改到字節
                    self.quota_bytes = int(user_data[2][1]) * 1024 * 1024
                    user_info_dic = {
                        'username': username,
                        'homedir': user_data[1][1],
                        'quota': user_data[2][1]
                    }
                    # 用戶的詳細信息發送到客戶端
                    conn.send(pickle.dumps(user_info_dic))
                    return True
                else:
                    conn.send(struct.pack('i', 0))  # 登陸失敗返回 0
            else:
                conn.send(struct.pack('i', 0))  # 登陸失敗返回 0


    def get_recv(self, conn):
        '''從client端接收發來的數據'''
        return pickle.loads(conn.recv(settings.max_recv_bytes))

    def current_home_size(self):
        """獲得當前用戶目錄的大小,字節/M"""
        self.home_bytes_size = 0
        File_func().recursion_file(self.homedir_path, self.home_bytes_size)
        home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)

  

file_func.py數據庫

# _*_ coding: utf-8 _*_
import os
import sys
import struct
import pickle

from config import settings
from core import ftp_server
from core import md5_func


class File_func(object):
    def readfile(self,file_path):
        '''讀取文件,獲得文件內容的bytes類型'''
        with open(file_path, 'rb') as f:
            filedata = f.read()
        return filedata

    def send_filedata(self,file_path,conn,exist_file_size=0):
        """下載時,將文件打開,send(data)"""
        with open(file_path, 'rb') as f:
            f.seek(exist_file_size)
            while True:
                data = f.read(1024)
                if data:
                    conn.send(data)
                else:
                    break

    def write_file(self,conn,f,recv_size,file_size):
        '''上傳文件時,將文件內容寫入到文件中'''
        while recv_size < file_size:
            res = conn.recv(settings.max_recv_bytes)
            f.write(res)
            recv_size += len(res)
            conn.send(struct.pack('i', recv_size))  # 爲了進度條的顯示

    def recursion_file(self, homedir_path,home_bytes_size):
        """遞歸查詢用戶目錄下的全部文件,算出文件的大小"""
        res = os.listdir(homedir_path)
        for i in res:
            path = os.path.join(homedir_path,i)
            if os.path.isdir(path):
                self.recursion_file(path,home_bytes_size)
            elif os.path.isfile(path):
                home_bytes_size += os.path.getsize(path)

    def current_home_size(self,homedir_path):
        """獲得當前用戶目錄的大小,字節/M"""
        self.home_bytes_size =0
        self.recursion_file(self.home_bytes_size,homedir_path)
        home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)

  

ftp_server.pyjson

# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import pickle
import subprocess
import hashlib
import queue
from threading import Thread
from threading import currentThread

from config import settings
from core.user_handle import UserHandle
from core.file_func import File_func
from core.md5_func import Md5_func
from core.auth_func import Auth_func
class FTPServer():

    def __init__(self,server_address,bind_and_listen = True):
        self.server_address = server_address
        self.socket = socket.socket(settings.address_family,settings.socket_type)
        self.q = queue.Queue(settings.MAX_CONCURRENT_COUNT)
        if bind_and_listen:
            try:
                self.server_bind()
                self.server_listen()
            except Exception:
                self.server_close()

    def server_bind(self):
        allow_reuse_address = False
        if allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)

    def server_listen(self):
        self.socket.listen(settings.listen_count)

    def server_close(self):
        self.socket.close()

    def server_accept(self):
        return self.socket.accept()

    def conn_close(self, conn):
        conn.close()

    def server_link(self):
        print("\033[31;1mwaiting client .....\033[0m")
        while True:  # 連接循環
            conn,self.client_addr = self.server_accept()
            print('客戶端地址:', self.client_addr)
            # while True:  # 通訊循環
            try:
                t = Thread(target=self.server_handle,args=(conn,))
                self.q.put(t)
                t.start()
            except Exception as e:
                print(e)
                self.conn_close(conn)
                self.q.get()

    def server_handle(self,conn):
        '''處理與用戶的交互指令'''
        if Auth_func().auth(conn):
            print("\033[32;1m-------user authentication successfully-------\033[0m")
            try:
                res = conn.recv(settings.max_recv_bytes)
                if not res:
                    self.conn_close(conn)
                    self.q.get()
                # 解析命令,提取相應的參數
                self.cmds = res.decode(settings.coding).split()
                if hasattr(self, self.cmds[0]):
                    func = getattr(self, self.cmds[0])
                    func(conn)
            except Exception as e:
                print(e)
                self.conn_close(conn)
                self.q.get()
  
    def get(self, conn):
        '''
       下載,首先查看文件是否存在,而後上傳文件的報頭大小,上傳文件,以讀的方式發開文件
       找到下載的文件
        發送 header_size
        發送 header_bytes file_size
        讀文件 rb 發送 send(line)
        若文件不存在,發送0 client提示:文件不存在
       :param cmds:
       :return:
               '''
        if len(self.cmds) > 1:
            filename = self.cmds[1]
            self.file_path = os.path.join(os.getcwd(), filename)
            if os.path.isfile(self.file_path):
                file_size = os.path.getsize(self.file_path)
                obj = conn.recv(4)
                exist_file_size = struct.unpack('i', obj)[0]
                header = {
                    'filename': filename,
                    'filemd5': Md5_func().getfile_md5(self.file_path),
                    'file_size': file_size
                }
                header_bytes = pickle.dumps(header)
                conn.send(struct.pack('i', len(header_bytes)))
                conn.send(header_bytes)
                if exist_file_size:  # 表示以前被下載過 一部分
                    if exist_file_size != file_size:
                        File_func().send_filedata(self.file_path, exist_file_size)
                    else:
                        print('\033[31;1mbreakpoint and file size are the same\033[0m')
                else:  # 文件第一次下載
                    File_func().send_filedata(self.file_path, conn)
            else:
                print('\033[31;1merror\033[0m')
                conn.send(struct.pack('i', 0))

        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def put(self, conn):
        """從client上傳文件到server當前工做目錄下
        """
        if len(self.cmds) > 1:
            obj = conn.recv(4)
            state_size = struct.unpack('i', obj)[0]
            if state_size:
                # 算出了home下已被佔用的大小self.home_bytes_size
                self.current_home_size()
                header_bytes = conn.recv(struct.unpack('i', conn.recv(4))[0])
                header_dic = pickle.loads(header_bytes)
                filename = header_dic.get('filename')
                file_size = header_dic.get('file_size')
                file_md5 = header_dic.get('file_md5')
                self.file_path = os.path.join(os.getcwd(), filename)
                if os.path.exists(self.file_path):
                    conn.send(struct.pack('i', 1))
                    has_size = os.path.getsize(self.file_path)
                    if has_size == file_size:
                        print("\033[31;1mfile already does exist!\033[0m")
                        conn.send(struct.pack('i', 0))
                    else:
                        print('\033[31;1mLast time  file not finished,this time continue\033[0m')
                        conn.send(struct.pack('i', 1))
                        if self.home_bytes_size + int(file_size - has_size) > self.quota_bytes:
                            print('\033[31;1mSorry exceeding user quotas\033[0m')
                            conn.send(struct.pack('i', 0))
                        else:
                            conn.send(struct.pack('i', 1))
                            conn.send(struct.pack('i', has_size))
                            with open(self.file_path, 'ab') as f:
                                f.seek(has_size)
                                File_func().write_file(conn, f, has_size, file_size)
                            Md5_func().verification_filemd5(self.file_path, conn, file_md5)

                else:
                    conn.send(struct.pack('i', 0))
                    print('\033[31;1mfile does not exist, now first put\033[0m')
                    if self.home_bytes_size + int(file_size) > self.quota_bytes:
                        print('\033[31;1mSorry exceeding user quotas\033[0m')
                        conn.send(struct.pack('i', 0))
                    else:
                        conn.send(struct.pack('i', 1))
                        with open(self.file_path, 'wb') as f:
                            recv_size = 0
                            File_func().write_file(conn, f, recv_size, file_size)
                        Md5_func().verification_filemd5(self.file_path,conn, file_md5)


            else:
                print("\033[31;1mfile does not exist!\033[0m")
        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def ls(self, conn):
        '''查看當前工做目錄下,先返回文件列表的大小,在返回查詢的結果'''
        print("\033[34;1mview current working directory\033[0m")
        subpro_obj = subprocess.Popen('dir', shell=True,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
        stdout = subpro_obj.stdout.read()
        stderr = subpro_obj.stderr.read()
        conn.send(struct.pack('i', len(stdout + stderr)))
        conn.send(stdout)
        conn.send(stderr)
        print('\033[31;1mCongratulations view directory success\033[0m')

    def mkdir(self, conn):
        '''增長目錄
        在當前目錄下,增長目錄
        1.查看目錄名是否已經存在
        2.增長目錄成功,返回 1
        2.增長目錄失敗,返回 0'''
        print("\033[34;1madd working directory\033[0m")
        if len(self.cmds) > 1:
            mkdir_path = os.path.join(os.getcwd(), self.cmds[1])
            if not os.path.exists(mkdir_path):
                os.mkdir(mkdir_path)
                print('\033[31;1mCongratulations add directory success\033[0m')
                conn.send(struct.pack('i', 1))
            else:
                print("\033[31;1muser directory already does exist\033[0m")
                conn.send(struct.pack('i', 0))
        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def cd(self, conn):
        '''切換目錄
        1.查看是不是目錄名
        2.拿到當前目錄,拿到目標目錄,
        3.判斷homedir是否在目標目錄內,防止用戶越過本身的home目錄 eg: ../../....
        4.切換成功,返回 1
        5.切換失敗,返回 0'''
        print("\033[34;1mSwitch working directory\033[0m")
        if len(self.cmds) > 1:
            dir_path = os.path.join(os.getcwd(), self.cmds[1])
            if os.path.isdir(dir_path):
                # os.getcwd 獲取當前工做目錄
                previous_path = os.getcwd()
                # os.chdir改變當前腳本目錄
                os.chdir(dir_path)
                target_dir = os.getcwd()
                if self.homedir_path in target_dir:
                    print('\033[31;1mCongratulations switch directory success\033[0m')
                    conn.send(struct.pack('i', 1))
                else:
                    print('\033[31;1mSorry switch directory failed\033[0m')
                    # 切換失敗後,返回到以前的目錄下
                    os.chdir(previous_path)
                    conn.send(struct.pack('i', 0))
            else:
                print('\033[31;1mSorry switch directory failed,the directory is not current directory\033[0m')
                conn.send(struct.pack('i', 0))
        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def remove(self, conn):
        """刪除指定的文件,或者空文件夾
               1.刪除成功,返回 1
               2.刪除失敗,返回 0
               """
        print("\033[34;1mRemove working directory\033[0m")
        if len(self.cmds) > 1:
            file_name = self.cmds[1]
            file_path = os.path.join(os.getcwd(), file_name)
            if os.path.isfile(file_path):
                os.remove(file_path)
                conn.send(struct.pack('i', 1))
            elif os.path.isdir(file_path):  # 刪除空目錄
                if not len(os.listdir(file_path)):
                    os.removedirs(file_path)
                    print('\033[31;1mCongratulations remove success\033[0m')
                    conn.send(struct.pack('i', 1))
                else:
                    print('\033[31;1mSorry remove directory failed\033[0m')
                    conn.send(struct.pack('i', 0))
            else:
                print('\033[31;1mSorry remove directory failed\033[0m')
                conn.send(struct.pack('i', 0))
        else:
            print("\033[31;1muser does not enter file name\033[0m")

  

main.py安全

# _*_ coding: utf-8 _*_ 
from core.user_handle import UserHandle
from core.ftp_server import FTPServer
from config import settings


class Manager():
    '''
    主程序,包括啓動server,建立用戶,退出
    :return:
    '''
    def __init__(self):
        pass

    def start_ftp(self):
        '''啓動server端'''
        server = FTPServer(settings.ip_port)
        server.server_link()
        server.close()

    def create_user(self):
        '''建立用戶,執行建立用戶的類'''
        username =  input("\033[32;1mplease input your username>>>\033[0m").strip()
        UserHandle(username).add_user()

    def logout(self):
        '''
        退出登錄
        :return:
        '''
        print("\033[32;1m-------Looking forward to your next login-------\033[0m")
        exit()

    def interactive(self):
        '''交互函數'''
        msg = '''\033[32;1m
                       1   啓動ftp服務端
                       2   建立用戶
                       3   退出
               \033[0m'''
        menu_dic = {
            "1": 'start_ftp',
            "2": 'create_user',
            "3": 'logout',
        }
        exit_flag = False
        while not exit_flag:
            print(msg)
            user_choice = input("Please input a command>>>").strip()
            if user_choice in menu_dic:
                getattr(self,menu_dic[user_choice])()
            else:
                print("\033[31;1myou choice doesn't exist\033[0m")

  

md5_func.py服務器

# _*_ coding: utf-8 _*_
import hashlib
import struct

from core import ftp_server
from core import file_func

class Md5_func(object):

    def getfile_md5(self,file_path):
        '''獲取文件的md5'''
        md5 = hashlib.md5(file_func.File_func().readfile(file_path))
        print("md5是:\n",md5.hexdigest())
        return md5.hexdigest()

    def handle_data(self):
        '''處理接收到的數據,主要是將密碼轉化爲md5的形式'''
        user_dic = ftp_server.FTPServer().get_recv()
        username = user_dic['username']
        password = user_dic['password']
        md5_obj = hashlib.md5()
        md5_obj.update(password)
        check_password = md5_obj.hexdigest()

    def verification_filemd5(self,file_path,conn,filemd5):
        # 判斷文件內容的md5
        if self.getfile_md5(file_path) == filemd5:
            print('\033[31;1mCongratulations download success\033[0m')
            conn.send(struct.pack('i', 1))
        else:
            print('\033[31;1mSorry download failed\033[0m')
            conn.send(struct.pack('i', 0))

  

user_handle.py多線程

 

#_*_coding:utf-8_*_
import configparser
import hashlib
import os

from config import settings

class UserHandle():
    '''
    建立用戶名稱,密碼
    若是用戶存在,則返回,若是用戶不存在,則註冊成功
    '''
    def __init__(self,username):
        self.username = username
        self.config = configparser.ConfigParser()
        self.config.read(settings.ACCOUNTS_FILE)

    def password(self):
        '''生成用戶的密碼,而後加密'''
        password_inp = input("\033[32;1mplease input your password>>>\033[0m").strip()
        md5_obj = hashlib.md5()
        md5_obj.update(password_inp.encode('utf-8'))
        md5_password = md5_obj.hexdigest()
        return md5_password

    def disk_quota(self):
        '''生成每一個用戶的磁盤配額'''
        quota = input('\033[32;1mplease input Disk quotas>>>:\033[0m').strip()
        if quota.isdigit():
            return quota
        else:
            exit('\033[31;1mdisk quotas must be integer\033[0m')

    def add_user(self):
        '''建立用戶,存到accounts.ini'''
        if not self.config.has_section(self.username):
            print('\033[31;1mcreating username is :%s \033[0m' % self.username)
            self.config.add_section(self.username)
            self.config.set(self.username,'password',self.password())
            self.config.set(self.username, 'homedir', 'home/' + self.username)
            self.config.set(self.username, 'quota', self.disk_quota())
            self.write_config()
            self.create_userhome()
            print('\033[1;32msuccessfully create userdata\033[0m')
        else:
            print('\033[1;31musername already existing\033[0m')

    def create_userhome(self):
        '''建立用戶的home文件夾'''
        os.mkdir(os.path.join(settings.BASE_DIR, 'home', self.username))

    def write_config(self):
        '''寫入文檔'''
        with open(settings.ACCOUNTS_FILE,'w') as f:
            self.config.write(f)

    def judge_user(self):
        '''判斷用戶是否存在'''
        if self.config.has_section(self.username):
            return self.config.items(self.username)

  

2,client

2.1bin

run.py併發

# _*_ 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 ftp_client
from config import settings
if __name__ == '__main__':
    run = ftp_client.FTPClient(settings.ip_port)
    run.execute()

  

2.2config

settings.py

# _*_ coding: utf-8 _*_
import os
import sys
import socket

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
# 下載的文件存放路徑
down_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'download')
# 上傳的文件存放路徑
upload_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'upload')
#綁定的IP地址
BIND_HOST = '127.0.0.1'
#綁定的端口號
BIND_PORT = 9999
ip_port = (BIND_HOST,BIND_PORT)
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM

coding = 'utf-8'
listen_count = 5
max_recv_bytes = 8192
allow_reuser_address = False

  

2.3core

file_func.py

# _*_ coding: utf-8 _*_
import os
import sys
import struct
import pickle
from config import settings
from core import ftp_client
from core import md5_func
from core import progress_bar_func

class File_func(object):

    def readfile(self,file_path):
        '''讀取文件'''
        with open(file_path,'rb') as f:
            filedata = f.read()
        return filedata

    def appendfile_content(self,socket,file_path,temp_file_size,file_size):
        '''追加文件內容'''
        with open(file_path,'ab') as f:
            f.seek(temp_file_size)
            get_size = temp_file_size
            while get_size < file_size:
                res = socket.recv(settings.max_recv_bytes)
                f.write(res)
                get_size += len(res)
                progress_bar_func.progress_bar(1,get_size,file_size)  #1表示下載

    def write_file(self,socket,f,get_size,file_size):
        '''下載文件,將內容寫入文件中'''
        while get_size < file_size:
            res = socket.recv(settings.max_recv_bytes)
            f.write(res)
            get_size += len(res)
            progress_bar_func.progress_bar(1,get_size,file_size)  #1表示下載

    def recv_file_header(self,socket,header_size):
        """接收文件的header, filename file_size file_md5"""
        header_types = socket.recv(header_size)
        header_dic = pickle.loads(header_types)
        print(header_dic, type(header_dic))
        total_size = header_dic['file_size']
        filename = header_dic['filename']
        filemd5 = header_dic['filemd5']
        return (filename,total_size,filemd5)

    def open_sendfile(self,file_size,file_path,socket,recv_size =0):
        '''打開要上傳的文件(因爲本程序上傳文件的原理是先讀取本地文件,再寫到上傳地址的文件)'''
        with open(file_path, 'rb') as f:
            f.seek(recv_size)
            while True:
                data = f.read(1024)
                if data:
                    socket.send(data)
                    obj = socket.recv(4)
                    recv_size = struct.unpack('i', obj)[0]
                    progress_bar_func.progress_bar(2, recv_size, file_size)
                else:
                    break
        success_state = struct.unpack('i', socket.recv(4))[0]
        if success_state:
            print('\033[31;1mCongratulations upload success\033[0m')
        else:
            print('\033[31;1mSorry upload directory failed\033[0m')

  

ftp_client.py

# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import sys
import pickle
import hashlib

from config import settings
from core.md5_func import Md5_func
from core.file_func import File_func
from core import progress_bar_func
class FTPClient:

    def __init__(self,server_address,connect = True):
        self.server_address = server_address
        self.socket = socket.socket(settings.address_family,settings.socket_type)
        if connect:
            try:
                self.client_connect()
            except Exception:
                self.client_close()

    def client_connect(self):
        try:
            self.socket.connect(self.server_address)
        except Exception as e:
            print("\033[31;1merror:%s\033[0m"%e)
            exit("\033[31;1m\nThe server is not activated \033[0m")

    def client_close(self):
        self.socket.close()

    def get(self,cmds):
        """從server下載文件到client
        """
        if len(cmds) >1:
            filename = cmds[1]
            self.file_path = os.path.join(settings.down_filepath, filename)
            if os.path.isfile(self.file_path):  #若是文件存在,支持斷電續傳
                temp_file_size = os.path.getsize(self.file_path)
                self.socket.send(struct.pack('i',temp_file_size))
                header_size = struct.unpack('i',self.socket.recv(4))[0]
                if header_size:
                    filename,file_size,filemd5 = File_func().recv_file_header(self.socket,header_size)
                    if temp_file_size == file_size:
                        print('\033[34;1mFile already does exist\033[0m')
                    else:
                        print('\033[34;1mFile now is breakpoint continuation\033[0m')
                        File_func().appendfile_content(self.socket,self.file_path,temp_file_size,file_size)
                        Md5_func().verification_filemd5(self.file_path,filemd5)
                else:
                    print("\033[34;1mFile was downloaded before,but now server's file is not exist\033[0m")
            else:#若是文件不存在,則是直接下載
                self.socket.send(struct.pack('i',0))
                obj = self.socket.recv(1024)
                header_size = struct.unpack('i', obj)[0]
                if header_size==0:
                    print("\033[31;1mfile does not exist!\033[0m")
                else:
                    filename, file_size, filemd5 = File_func().recv_file_header(self.socket,header_size)
                    download_filepath = os.path.join(settings.down_filepath, filename)
                    with open(download_filepath, 'wb') as f:
                        get_size = 0
                        File_func().write_file(self.socket,f, get_size, file_size)
                    Md5_func().verification_filemd5(self.file_path,filemd5)
        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def ls(self,cmds):
        '''查看當前工做目錄,文件列表'''
        print("\033[34;1mview current working directory\033[0m")
        obj = self.socket.recv(4)
        dir_size = struct.unpack('i',obj)[0]
        recv_size = 0
        recv_bytes = b''
        while recv_size <dir_size:
            temp_bytes = self.socket.recv(settings.max_recv_bytes)
            recv_bytes +=temp_bytes
            recv_size += len(temp_bytes)
        print(recv_bytes.decode('gbk'))
        print('\033[31;1mCongratulations view directory success\033[0m')

    def mkdir(self,cmds):
        '''增長目錄
        1,server返回1 增長成功
        2,server返回2 增長失敗'''
        print("\033[34;1madd working directory\033[0m")
        obj = self.socket.recv(4)
        res = struct.unpack('i',obj)[0]
        if res:
            print('\033[31;1mCongratulations add directory success\033[0m')
        else:
            print('\033[31;1mSorry add directory failed\033[0m')

    def cd(self,cmds):
        '''切換目錄'''
        print("\033[34;1mSwitch working directory\033[0m")
        if len(cmds) >1:
            obj = self.socket.recv(4)
            res = struct.unpack('i', obj)[0]
            if res:
                print('\033[31;1mCongratulations switch directory success\033[0m')
            else:
                print('\033[31;1mSorry switch directory failed\033[0m')
        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def remove(self,cmds):
        '''表示刪除文件或空文件夾'''
        print("\033[34;1mRemove working directory\033[0m")
        obj = self.socket.recv(4)
        res = struct.unpack('i', obj)[0]
        if res:
            print('\033[31;1mCongratulations remove success\033[0m')
        else:
            print('\033[31;1mSorry remove directory failed\033[0m')

    def put_situation(self,file_size,condition=0):
        '''上傳的時候有兩種狀況,文件已經存在,文件不存在'''
        quota_state= struct.unpack('i', self.socket.recv(4))[0]
        if quota_state:
            if condition:
                obj = self.socket.recv(4)
                recv_size = struct.unpack('i', obj)[0]
                File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0)
            else:
                File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0)
        else:
            print('\033[31;1mSorry exceeding user quotas\033[0m')

    def put(self,cmds):
        """往server端登陸的用戶目錄下上傳文件
        """
        if len(cmds) > 1:
            filename = cmds[1]
            file_path = os.path.join(settings.upload_filepath, filename)
            if os.path.isfile(file_path):  # 若是文件存在,支持斷電續傳
                self.socket.send(struct.pack('i', 1))
                self.file_path = file_path
                file_size = os.path.getsize(self.file_path)
                header_dic = {
                    'filename': os.path.basename(filename),
                    'file_md5':  Md5_func().getfile_md5(self.file_path),
                    'file_size': file_size
                }
                header_bytes = pickle.dumps(header_dic)
                self.socket.send(struct.pack('i', len(header_bytes)))
                self.socket.send(header_bytes)
                state = struct.unpack('i', self.socket.recv(4))[0]
                if state:  #已經存在
                    has_state = struct.unpack('i', self.socket.recv(4))[0]
                    if has_state:
                        self.put_situation(file_size, 1)
                    else:  # 存在的大小 和文件大小一致 沒必要再傳
                        print("\033[31;1mfile already does exist!\033[0m")
                else:  # 第一次傳
                        self.put_situation(file_size)
            else:  # 文件不存在
                print("\033[31;1mfile does not exist!\033[0m")
                self.socket.send(struct.pack('i', 0))
        else:
            print("\033[31;1muser does not enter file name\033[0m")

    def get_recv(self):
        '''從client端接受發來的數據'''
        return pickle.loads(self.socket.recv(settings.max_recv_bytes))

    def login(self):
        '''
        登錄函數,當登錄失敗超過三次,則退出
        用戶密碼發送到server短
        接受server端返回的信息,若是成功返回1,失敗返回0
        :return: 若是用戶帳號密碼正確,則返回用戶數據的字典
        '''
        retry_count = 0
        while retry_count <3:
            username = input('\033[34;1mplease input Username:\033[0m').strip()
            if not username:
                continue
            password = input('\033[34;1mplease input Password:\033[0m').strip()
            user_dic = {
                'username':username,
                'password':password
            }
            #將用戶信息發送到客戶端,而後接受客戶端的數據
            data = pickle.dumps(user_dic)
            self.socket.send(pickle.dumps(user_dic))
            #爲了防止出現黏包問題,因此先解壓報頭,讀取報頭,再讀數據
            obj = self.socket.recv(4)
            res = struct.unpack('i',obj)[0]
            #此處,若是返回的是代碼4001,則成功 4002則失敗
            if res:
                print("\033[32;1m-----------------welcome to ftp client-------------------\033[0m")
                user_info_dic = self.get_recv()
                recv_username = user_info_dic['username']
                return True
            else:
                print("\033[31;1mAccount or Passwordoes not correct!\033[0m")
        retry_count +=1

    def execute(self):
        '''
        執行,或者實施
        :return:
        '''
        if self.login():
            while True:
                try:
                    self.help_info()
                    inp = input("Please input a command>>>").strip()
                    if not inp:
                        continue
                    self.socket.send(inp.encode(settings.coding))
                    cmds = inp.split()
                    if hasattr(self, cmds[0]):
                        func = getattr(self, cmds[0])
                        func(cmds)
                        break
                    else:
                        print('\033[31;1mNo such command ,please try again\033[0m')
                except Exception as e:  # server關閉了
                    print('\033[31;1m%s\033[0m'%e)
                    break

    def help_info(self):
        print ('''\033[34;1m
              get + (文件名)    表示下載文件
              put + (文件名)    表示上傳文件
              ls                 表示查詢當前目錄下的文件列表(只能訪問本身的文件列表) 
              mkdir + (文件名)  表示建立文件夾  
              cd + (文件名)     表示切換目錄(只能在本身的文件列表中切換)
              remove + (文件名) 表示刪除文件或空文件夾
        \033[0m''')

  

md5_func.py

# _*_ coding: utf-8 _*_
import hashlib
from core import ftp_client
from core.file_func import File_func

class Md5_func(object):
    def getfile_md5(self,file_path):
        '''對文件內容進行加密,也就是保持文件的一致性'''
        md5 = hashlib.md5(File_func().readfile(file_path))
        print("md5是:\n",md5.hexdigest())
        return md5.hexdigest()

    def verification_filemd5(self,file_path,filemd5):
        # 判斷下載下來的文件MD5值和server傳過來的MD5值是否一致
        if self.getfile_md5(file_path) == filemd5:
            print('\033[31;1mCongratulations download success\033[0m')
        else:
            print('\033[31;1mSorry download failed,download again support breakpoint continuation\033[0m')

  

progress_bar_fun.py

 

# _*_ coding: utf-8 _*_
'''進度條的表示功能'''
import sys

def progress_bar(num, get_size, file_size):
    float_rate = float(get_size) / float(file_size)
    rate_num = round(float_rate * 100, 2)
    if num == 1:  # 1表示下載
        sys.stdout.write('\033[31;1m\rfinish downloaded perentage:{0}%\033[0m'.format(rate_num))
    elif num == 2:  # 2表示上傳
        sys.stdout.write('\033[31;1m\rfinish uploaded perentage:{0}%\033[0m'.format(rate_num))
    sys.stdout.flush()
相關文章
相關標籤/搜索