實現支持多用戶在線的FTP程序(C/S)

1. 需求

1. 用戶加密認證 2. 容許多用戶登陸 3. 每一個用戶都有本身的家目錄,且只能訪問本身的家目錄 4. 對用戶進行磁盤分配,每個用戶的可用空間能夠本身設置 5. 容許用戶在ftp server上隨意切換目錄 6. 容許用戶查看本身家目錄下的文件 7. 容許用戶上傳和下載,保證文件的一致性(md5) 8. 文件上傳、下載過程當中顯示進度條 9. 支持多併發的功能 10. 使用隊列queue模塊,實現線程池 11. 容許用戶配置最大的併發數,好比容許只有10併發用戶 升級需求:10%
1. 文件支持斷點續傳

 

2. 開發環境

  Python 3.7.3python

3. 軟件開發

客戶端:
    |-conf
        |-setting.py        # 配置文件,存放服務端ip和port, 客戶端下載文件的目錄等
    |-core
        |-main.py            # FTP客戶端功能
    |-files                    # 用戶下載, 上傳文件的存放目錄
        |-.download             # 目錄存放用戶未下載完的文件的配置文件
    |-ftp_client.py             # 客戶端啓動程序
    
服務端:
    |-conf
        |-settings.py            # 配置文件,存放服務端ip和port, 用戶目錄及用戶帳戶, 日誌目錄, 與用戶確認交互的狀態碼, 日誌配置文件等等
        |-accounts.ini            # 用戶帳戶相關的信息
    |-core
        |-handler_request.py    # 專門處理服務端就與客戶端的請求, 以及命令
        |-main.py                # FTP服務端專門與客戶端創建鏈接
        |-management.py            # 管理FTP的的啓動, 中止, 重啓等
        |-mythreadpool.py        # 使用queue實現的簡單版的線程池, 缺點: 線程不能重複利用
    |-home    
        |-egon                    # 用戶家目錄,每個用戶以用戶名做爲家目錄
            |-.upload             # 目錄存放用戶未上傳完的文件的配置文件信息
        |-....                    # 每一個用戶下都有: 用戶家目錄,每個用戶以用戶名做爲家目錄
            |-.upload             # 每一個用戶下都有: 用戶未上傳完的文件的配置文件信息
    |-ftp_client.py                 # 服務端啓動程序
目錄結構

 

4. 服務端與客戶端的啓動

11.打開cmd命令行終端22.python3+啓動文件路徑+startftpserver33.例子:4C:\Users\洋辣子>python3Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\ftp_server.pystartftpserver
服務端啓動
1 1. 打開cmd命令行終端 2 2. python3 + 啓動文件路徑 3 3. 例子: 4     C:\Users\洋辣子> python3 Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\client\ftp_client.py
客戶端啓動

5. 用戶配置信息

1 用戶名: 用戶密碼: 2 alex                    123
3 egon                    123
4 ly                    123
5 jzd                    123
6 shx                    123    
View Code

6. 全部功能測試 

(1) 登錄

1 username>>:egon 2 password>>:123
3 用戶名密碼正確, 認證成功!
View Code

 

(2) 查看全部命令所對應的幫助信息

查看方法:git

  • 命令 + –-helpshell

 1 [egon@localhost ~]# ls --help
 2 
 3  查看當前目錄下的文件:  4  ls  5  指定目錄下的文件(只能查看到本身家目錄的範圍):  6                 ls /我是egon的目錄  7             
 8 [egon@localhost ~]# cd --help
 9 
10  相對路徑切換: 11                 cd /我是egon的目錄 12                     cd /我是江傻子的目錄 13  切換到上一層目錄: 14  cd .. 15  絕對路徑切換: 16                 cd /我是egon的目錄/我是江傻子的目錄 17  在當前目錄下切當前目錄: 18                 cd .
View Code

 

(3) ls: 查看

① 支持功能:

  • 查看當前目錄下的文件json

    • lswindows

  • 指定目錄下的文件(只能查看到本身家目錄的範圍)安全

    • ls /目錄1/目錄2服務器

  • 查看幫助信息網絡

    • ls /?併發

② 運行效果:

 1 [egon@localhost ~]# ls
 2  驅動器 Z 中的卷是 固態硬盤  3  卷的序列號是 AA26-64F0  4 
 5  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon 的目錄  6 
 7 2019-10-26  22:34    <DIR> .  8 2019-10-26  22:34    <DIR> ..  9 2019-10-19  20:03         1,081,540 123.docx 10 2019-10-27  13:45    <DIR> 我是egon的目錄 11                1 個文件      1,081,540 字節 12                3 個目錄 56,465,575,936 可用字節
1) 查看當前目錄下的文件

 

 

 1 [egon@localhost ~]# ls /我是egon的目錄
 2  驅動器 Z 中的卷是 固態硬盤  3  卷的序列號是 AA26-64F0  4 
 5  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄 的目錄  6 
 7 2019-10-27  13:45    <DIR> .  8 2019-10-27  13:45    <DIR> ..  9 2019-10-27  13:45    <DIR> 我是江傻子的目錄 10 2019-10-27  12:34 0 江傻子 11                1 個文件 0 字節 12                3 個目錄 56,465,575,936 可用字節
2) 指定目錄下的文件(只能查看到本身家目錄的範圍)

 

 1 [egon@localhost ~]# ls /?
 2 顯示目錄中的文件和子目錄列表。  3 
 4 DIR [drive:][path][filename] [/A[[:]attributes]] [/B] [/C] [/D] [/L] [/N]  5   [/O[[:]sortorder]] [/P] [/Q] [/R] [/S] [/T[[:]timefield]] [/W] [/X] [/4]  6 
 7  [drive:][path][filename]  8               指定要列出的驅動器、目錄和/或文件。  9 
10   /A 顯示具備指定屬性的文件。 11  屬性 D 目錄 R 只讀文件 12  H 隱藏文件 A 準備存檔的文件 13  S 系統文件 I 無內容索引文件 14  L 從新分析點 O 脫機文件 15                - 表示「否」的前綴 16   /B 使用空格式(沒有標題信息或摘要)。 17   /C          在文件大小中顯示千位數分隔符。這是默認值。用 /-C 來 18  禁用分隔符顯示。 19   /D 跟寬式相同,但文件是按欄分類列出的。 20   /L 用小寫。 21   /N 新的長列表格式,其中文件名在最右邊。 22   /O 用分類順序列出文件。 23  排列順序 N 按名稱(字母順序) S 按大小(從小到大) 24                E  按擴展名(字母順序)   D  按日期/時間(從先到後) 25                G  組目錄優先           - 反轉順序的前綴 26   /P 在每一個信息屏幕後暫停。 27   /Q 顯示文件全部者。 28   /R 顯示文件的備用數據流。 29   /S 顯示指定目錄和全部子目錄中的文件。 30   /T 控制顯示或用來分類的時間字符域 31  時間段 C 建立時間 32  A 上次訪問時間 33  W 上次寫入的時間 34   /W 用寬列表格式。 35   /X          顯示爲非 8dot3 文件名產生的短名稱。格式是 /N 的格式, 36  短名稱插在長名稱前面。若是沒有短名稱,在其位置則 37  顯示空白。 38   /4 以四位數字顯示年份 39 
40 能夠在 DIRCMD 環境變量中預先設定開關。經過添加前綴 - (破折號) 41 來替代預先設定的開關。例如,/-W。 42 
43 [egon@localhost ~]# 
3) 查看幫助信息

 

 

(4) cd: 切換目錄

① 支持功能:

  • 相對路徑切換app

    • cd /目錄1

      • cd /目錄2

  • 切換到上一層目錄

    • cd ..

  • 絕對路徑切換

    • cd /目錄1/目錄2

  • 在當前目錄下切當前目錄

    • cd .

② 運行效果:

 1 [egon@localhost ~]# cd /我是egon的目錄
 2 切換目錄成功  3 [egon@localhost /home/egon/我是egon的目錄]# ls
 4  驅動器 Z 中的卷是 固態硬盤  5  卷的序列號是 AA26-64F0  6 
 7  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄 的目錄  8 
 9 2019-10-27  13:45    <DIR> . 10 2019-10-27  13:45    <DIR> .. 11 2019-10-27  13:45    <DIR> 我是江傻子的目錄 12 2019-10-27  12:34 0 江傻子 13                1 個文件 0 字節 14                3 個目錄 56,465,563,648 可用字節 15 
16 [egon@localhost /home/egon/我是egon的目錄]# cd /我是江傻子的目錄
17 切換目錄成功 18 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# ls
19  驅動器 Z 中的卷是 固態硬盤 20  卷的序列號是 AA26-64F0 21 
22  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄\我是江傻子的目錄 的目錄 23 
24 2019-10-27  13:45    <DIR> . 25 2019-10-27  13:45    <DIR> .. 26 2019-10-27  13:45 0 我是江大傻.txt 27                1 個文件 0 字節 28                2 個目錄 56,465,563,648 可用字節 29 
30 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# 
1) 相對路徑切換

 

 1 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# cd ..
 2 切換目錄成功  3 [egon@localhost /home/egon/我是egon的目錄]# ls
 4  驅動器 Z 中的卷是 固態硬盤  5  卷的序列號是 AA26-64F0  6 
 7  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄 的目錄  8 
 9 2019-10-27  13:45    <DIR> . 10 2019-10-27  13:45    <DIR> .. 11 2019-10-27  13:45    <DIR> 我是江傻子的目錄 12 2019-10-27  12:34 0 江傻子 13                1 個文件 0 字節 14                3 個目錄 56,465,559,552 可用字節 15 
16 [egon@localhost /home/egon/我是egon的目錄]# 
2) 切換到上一層目錄

 

 

 1 [egon@localhost ~]# cd /我是egon的目錄/我是江傻子的目錄
 2 切換目錄成功  3 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# ls
 4  驅動器 Z 中的卷是 固態硬盤  5  卷的序列號是 AA26-64F0  6 
 7  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄\我是江傻子的目錄 的目錄  8 
 9 2019-10-27  13:45    <DIR> . 10 2019-10-27  13:45    <DIR> .. 11 2019-10-27  13:45 0 我是江大傻.txt 12                1 個文件 0 字節 13                2 個目錄 56,465,559,552 可用字節 14 
15 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# 
3) 絕對路徑切換

 

 1 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# cd .
 2 切換目錄成功  3 [egon@localhost /home/egon/我是egon的目錄/我是江傻子的目錄]# ls
 4  驅動器 Z 中的卷是 固態硬盤  5  卷的序列號是 AA26-64F0  6 
 7  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄\我是江傻子的目錄 的目錄  8 
 9 2019-10-27  13:45    <DIR> . 10 2019-10-27  13:45    <DIR> .. 11 2019-10-27  13:45 0 我是江大傻.txt 12                1 個文件 0 字節 13                2 個目錄 56,465,559,552 可用字節
4) 在當前目錄下切當前目錄

 

(5) mkdir: 建立目錄(支持遞歸建立目錄)

① 支持功能:

  • 相對路徑建立:

    • mkdir /目錄

  • 生成多層遞歸目錄:

    • mkdir /目錄1/目錄2

② 運行效果:

1 [egon@localhost ~]# mkdir /a
2 建立目錄成功!
1) 相對路徑建立:

 

1 [egon@localhost ~]# mkdir /a/b
2 建立目錄成功!
2) 絕對路徑建立:

 

(6) rmdir: 刪除空目錄

① 支持功能:

  • 刪除空目錄:

    • rmdir /目錄1/空目錄2

② 運行效果:

1 [egon@localhost ~]# rmdir /a/b
2 刪除目錄成功!
1) 刪除空目錄

 

(7) remove: 刪除文件

① 支持功能:

  • 刪除文件

    • remove /目錄1/文件

② 運行效果:

 1 [egon@localhost ~]# ls /我是egon的目錄/江傻子
 2  驅動器 Z 中的卷是 固態硬盤  3  卷的序列號是 AA26-64F0  4 
 5  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄 的目錄  6 
 7 2019-10-27  12:34 0 江傻子  8                1 個文件 0 字節  9                0 個目錄 56,465,010,688 可用字節 10 
11 [egon@localhost ~]# remove /我是egon的目錄/江傻子
12 刪除文件成功! 13 
14 [egon@localhost ~]# ls /我是egon的目錄/江傻子
15 找不到文件 16  驅動器 Z 中的卷是 固態硬盤 17  卷的序列號是 AA26-64F0 18 
19  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄 的目錄
1) 刪除文件

 

(8) upload: 上傳文件到服務端

① 支持功能:

  • 上傳到服務端當前路徑:

    • upload 文件

  • 經過cd切換目錄上傳文件到該目錄下

    • cd /目錄1/目錄2

      • upload 文件

② 運行效果:

 1 [egon@localhost ~]# upload 服務器管理綜合報告.docx
 2 你能夠上傳文件, 在您上傳以前, 您的目前空間:68.97MB!  3 
 4 upload running...  5 [##################################################] 100.00%
 6 upload succeed!  7 上傳文件成功, 您上傳完後的剩餘空間:66.07MB!  8 
 9 [egon@localhost ~]# ls
10  驅動器 Z 中的卷是 固態硬盤 11  卷的序列號是 AA26-64F0 12 
13  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon 的目錄 14 
15 2019-10-31  16:37    <DIR> . 16 2019-10-31  16:37    <DIR> .. 17 2019-10-31  16:37    <DIR> .upload 18 2019-10-31  16:09        32,535,704 03_函數調用的三種形式.mp4 19 2019-10-28  09:53    <DIR> 我是egon的目錄 20 2019-10-31  16:37         3,039,102 服務器管理綜合報告.docx 21                2 個文件     35,574,806 字節 22                4 個目錄 56,393,715,712 可用字節
1) 上傳到服務端當前路徑:

 

 1 [egon@localhost ~]# cd /我是egon的目錄
 2 切換目錄成功  3 
 4 [egon@localhost /我是的目錄]# upload 服務器管理綜合報告.docx
 5 你能夠上傳文件, 在您上傳以前, 您的目前空間:66.07MB!  6 
 7 upload running...  8 [##################################################] 100.00%
 9 upload succeed! 10 上傳文件成功, 您上傳完後的剩餘空間:63.17MB! 11 
12 
13 [egon@localhost /我是的目錄]# ls
14  驅動器 Z 中的卷是 固態硬盤 15  卷的序列號是 AA26-64F0 16 
17  Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\server\home\egon\我是egon的目錄 的目錄 18 
19 2019-10-31  16:47    <DIR> . 20 2019-10-31  16:47    <DIR> .. 21 2019-10-27  13:45    <DIR> 我是江傻子的目錄 22 2019-10-31  16:47         3,039,102 服務器管理綜合報告.docx 23                1 個文件      3,039,102 字節 24                3 個目錄 56,390,676,480 可用字節
經過cd切換目錄上傳文件到該目錄下:

 

(9) resume_upload: 續傳未上傳完成的文件到服務端

① 支持功能:

  • 繼續上傳文件到服務端當前路徑:

    • resume_upload 文件名

  • 經過cd切換目錄, 到該目錄下指定服務端的某個目錄下繼續上傳:

    • cd /目錄1/目錄2

      • resume_upload 文件名

② 運行效果:

 1 ------您的files文件夾下所含有的文件------
 2 1: .download  3 2: 03_函數調用的三種形式.mp4  4 3: 服務器管理綜合報告.docx  5 
 6 [egon@localhost ~]# upload 03_函數調用的三種形式.mp4
 7 你能夠上傳文件, 在您上傳以前, 您的目前空間:97.10MB!  8 
 9 upload running... 10 [############ ] 25.43%
先斷開傳輸:

 

 1 username>>:egon  2 password>>:123
 3 用戶名密碼正確, 認證成功!  4 您的還有爲上傳完的文件, 是否繼續上傳!  5 
 6             數量: 1  文件路徑: /03_函數調用的三種形式.mp4 文件名: 03_函數調用的三種形式.mp4  7                 文件原大小: 32535704字節 未完成的文件大小: 8273050字節 上傳的百分比: 25.43%
 8             
 9 
10 ------您的files文件夾下所含有的文件------
11 1: .download 12 2: 03_函數調用的三種形式.mp4 13 3: 服務器管理綜合報告.docx 14 
15 
16 [egon@localhost ~]# resume_upload 03_函數調用的三種形式.mp4
17 您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:89.21MB! 18 8273050
19 
20 upload running... 21 [##################################################] 100.00%
22 upload succeed! 23 上傳文件成功, 您上傳完後的剩餘空間:66.07MB!
1) 繼續上傳文件到服務端當前路徑:

 

 1 username>>:egon  2 password>>:123
 3 用戶名密碼正確, 認證成功!  4 您的還有爲上傳完的文件, 是否繼續上傳!  5 
 6             數量: 1  文件路徑: 的目錄/03_函數調用的三種形式.mp4 文件名: 03_函數調用的三種形式.mp4  7                 文件原大小: 32535704字節 未完成的文件大小: 12534221字節 上傳的百分比: 38.52%
 8             
 9 
10 ------您的files文件夾下所含有的文件------
11 1: .download 12 2: 03_函數調用的三種形式.mp4 13 3: 服務器管理綜合報告.docx 14 
15 [egon@localhost ~]# cd /我是egon的目錄
16 切換目錄成功 17 
18 ------您的files文件夾下所含有的文件------
19 1: .download 20 2: 03_函數調用的三種形式.mp4 21 3: 服務器管理綜合報告.docx 22 
23 [egon@localhost /我是的目錄]# resume_upload 03_函數調用的三種形式.mp4
24 您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:66.07MB! 25 
26 upload running... 27 [##################################################] 100.00%
28 upload succeed! 29 上傳文件成功, 您上傳完後的剩餘空間:47.00MB!
2) 經過cd切換目錄, 到該目錄下指定服務端的某個目錄下繼續上傳:

 

(10) download: 下載文件

① 支持功能:

  • 從服務端當前目錄下下載文件

    • download 文件

  • 從服務端絕對路徑下下載文件

    • download /目錄1/文件

② 運行效果:

1 [egon@localhost ~]# download 服務器管理綜合報告.docx
2 
3 download run... 4 [##################################################] 100.00%
5 download succeed!
1) 從服務端當前目錄下下載文件

 

1 [egon@localhost ~]# download /我是egon的目錄/03_函數調用的三種形式.mp4
2 
3 download run... 4 [##################################################] 100.00%
5 download succeed!
2) 從服務端絕對路徑下下載文件

 

(11) 在download基礎之上: 繼續從服務端續傳下載文件

① 支持功能:

  • 用戶登錄的時候顯示爲下載完的文件

    • 用戶根據序號選擇要繼續續傳的文件

  • 用戶能夠屢次循環選擇

  • 支持斷點之後據續斷點續傳

② 運行效果:

 1 username>>:egon  2 password>>:123
 3 用戶名密碼正確, 認證成功!  4 服務端檢測您沒有未上傳完成的文件!  5 檢測到您本地還有未上傳完成的文件  6 --------------------------------------------------------------------未完成續傳的數量: 2個---------------------------------------------------------------------
 7 序號: 1
 8 
 9  未完成的文件路徑: Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\client\files\03_函數調用的三種形式.mp4.download 文件名: 03_函數調用的三種形式.mp4 10             文件原大小: 32535704字節      已完成的文件大小: 3511466字節     上傳的百分比: 10.79%
11             
12 序號: 2
13 
14  未完成的文件路徑: Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\client\files\服務器管理綜合報告.docx.download 文件名: 服務器管理綜合報告.docx 15             文件原大小: 3039102字節      已完成的文件大小: 712297字節     上傳的百分比: 23.44%
16             
17 [退出: q/Q]請根據序號選擇您是否繼續下載沒有完成的文件>>:1
18 開始續傳...... 19 
20 download run... 21 [##################################################] 100.00%
22 download succeed! 23 
24 續傳完畢! 25 --------------------------------------------------------------------未完成續傳的數量: 1個---------------------------------------------------------------------
26 序號: 1
27 
28  未完成的文件路徑: Z:\pycharm\開發FTP程序之路\第2次FTP_修改後的內容\第二次實現方式\client\files\服務器管理綜合報告.docx.download 文件名: 服務器管理綜合報告.docx 29             文件原大小: 3039102字節      已完成的文件大小: 712297字節     上傳的百分比: 23.44%
30             
31 [退出: q/Q]請根據序號選擇您是否繼續下載沒有完成的文件>>:1
32 開始續傳...... 33 
34 download run... 35 [##################################################] 100.00%
36 download succeed! 37 
38 續傳完畢! 39 
40 ------您的files文件夾下所含有的文件------
41 1: .download 42 2: 03_函數調用的三種形式.mp4 43 3: 服務器管理綜合報告.docx
View Code

 

(12) 爲用戶磁盤配額

 1 [egon@localhost /我是的目錄/我是江傻子的目錄/目錄1]# upload 03_函數調用的三種形式.mp4
 2 你能夠上傳文件, 在您上傳以前, 您的目前空間:35.04MB!  3 
 4 upload running...  5 [##################################################] 100.00%
 6 upload succeed!  7 上傳文件成功, 您上傳完後的剩餘空間:4.02MB!  8 
 9 
10 [egon@localhost /我是的目錄/我是江傻子的目錄/目錄2]# upload 03_函數調用的三種形式.mp4
11 上傳文件失敗, 您的空間不足, 您的剩餘空間:4.02MB!
View Code

 

(13) 使用隊列queue模塊,實現線程, 容許用戶配置最大的併發數5個

(14) 記錄了日誌功能

① 終端打印

② 保存文件之中 

 

7. 不足之處

  • 沒有實現多文件, 以及多文件夾打包上傳

  • client用戶暫時只能用files文件夾下的路徑進行上傳下載, 不能動態指定

8.代碼展現

① client

1) conf

 1 import os  2 
 3 BASE_DIR = os.path.normpath(os.path.join(__file__, '..', '..'))  4 
 5 FILES_PATH = os.path.join(BASE_DIR, 'files')  6 UNFINISHED_DOWNLOAD_FILES_PATH = os.path.join(FILES_PATH, '.download', 'unfinished.shv')  7 
 8 HOST = '127.0.0.1'
 9 PORT = 8080
10 
11 
12 help_dic = { 13     'ls --help': """
14  查看當前目錄下的文件: 15  ls 16  指定目錄下的文件(只能查看到本身家目錄的範圍): 17  ls /目錄1/目錄2 18  查看ls的詳細幫助: 19  ls /? 20             """, 21     'cd --help': """
22  相對路徑切換: 23  cd /目錄1 24  cd /目錄2 25  絕對路徑切換: 26  cd /目錄1/目錄2 27  切換到上一層目錄: 28  cd .. 29  在當前目錄下切當前目錄: 30  cd . 31             """, 32     'mkdir --help': """
33  相對路徑建立: 34  mkdir /目錄 35  生成多層遞歸目錄: 36  mkdir /目錄1/目錄2 37             """, 38     'rmdir --help': """
39  刪除空目錄: 40  rmdir /目錄1/空目錄2 41             """, 42     'remove --help': """
43  刪除文件: 44  remove /目錄1/文件 45             """, 46     'upload --help': """
47  上傳到服務端當前路徑: 48  upload 文件名 49  經過cd切換目錄上傳文件到該目錄下: 50  cd /目錄1/目錄2 51  upload 文件 52             """, 53     'resume_upload --help': """
54  繼續上傳文件到服務端當前路徑: 55  resume_upload 文件名 56  經過cd切換目錄, 到該目錄下指定服務端的某個目錄下繼續上傳: 57  cd /目錄1/目錄2 58  resume_upload 文件名 59                 """, 60     None: """
61  查看相對應的幫助信息: 62  1. ls + --help 63  2. cd --help 64  3. mkdir --help 65  4. rmdir --help 66  5. remove --help 67  6. upload --help 68  7. resume_upload --help 69             """, 70 }
settings.py

2) core

 1 import hashlib  2 import json  3 import os  4 import shelve  5 import socket  6 import struct  7 
 8 from conf import settings  9 
 10 
 11 class FTPClient:  12     """FTP客戶端."""
 13     address_family = socket.AF_INET  14     socket_type = socket.SOCK_STREAM  15     max_packet_size = 8192
 16     encoding = 'utf-8'
 17     windows_encoding = 'gbk'
 18 
 19     struct_fmt = 'i'
 20     fixed_packet_size = 4
 21 
 22     def __init__(self, server_address, connect=True):  23         self.server_address = server_address  24         self.socket = socket.socket(self.address_family, self.socket_type)  25 
 26         self.breakpoint_resume = shelve.open(settings.UNFINISHED_DOWNLOAD_FILES_PATH)  27 
 28         self.username = None  29         self.current_dir = '~'
 30         if connect:  31             try:  32  self.client_connect()  33             except Exception:  34  self.client_close()  35                 raise
 36 
 37     def client_connect(self):  38         """客戶端鏈接服務端ip和port."""
 39  self.socket.connect(self.server_address)  40 
 41     def client_close(self):  42         """關閉鏈接通道."""
 43  self.socket.close()  44 
 45     def interactive(self):  46         """與服務端進行全部的交互."""
 47         if self.auth():  48  self.unfinished_file_check()  49             while True:  50  self.show_str()  51                 msg = input('[%s@localhost %s]# ' % (self.username, self.current_dir)).strip()  52                 if not msg:  53                     continue
 54                 if not self.help_msg(msg):  55                     continue
 56                 # 覈驗命令參數
 57                 cmd, path = self.verify_args(msg)  58                 if hasattr(self, '_%s' % cmd):  59                     func = getattr(self, '_%s' % cmd)  60  func(path)  61                 else:  62  self.help_msg()  63 
 64  @staticmethod  65     def verify_args(msg):  66         """
 67  效驗參數.  68  :param msg: ls 或 ls /路徑 或 ls /路徑1/路徑2/  69  :return: (ls, []) 或 (ls, ['路徑']) 或 (ls, ['路徑1', '路徑2'])  70         """
 71         cmd_args = msg.split()  72         cmd, path = cmd_args[0], cmd_args[1:]  73         if path:  74             path = ''.join(cmd_args[1:]).strip('//').split('/')  75         # print('cmd, path:', cmd, path)
 76         return cmd, path  77 
 78     def unfinished_file_check(self):  79         if not list(self.breakpoint_resume.keys()):  80             return
 81 
 82         print('檢測到您本地還有未上傳完成的文件')  83         unfinished_path_list = []  84         msg_list = []  85         for unfinished_file_path in self.breakpoint_resume.keys():  86             file_name = self.breakpoint_resume[unfinished_file_path]['file_name']  87             file_size = self.breakpoint_resume[unfinished_file_path]['file_size']  88             unfinished_file_size = os.path.getsize(unfinished_file_path)  89             percent = unfinished_file_size / file_size * 100
 90             path = self.breakpoint_resume[unfinished_file_path]['path']  91             dic = {'unfinished_file_size': unfinished_file_size, 'path': path}  92  unfinished_path_list.append(dic)  93             msg = """
 94  未完成的文件路徑: %s 文件名: %s  95  文件原大小: %s字節 已完成的文件大小: %s字節 上傳的百分比: %.2f%%  96             """ % (unfinished_file_path, file_name, file_size, unfinished_file_size, percent)  97  msg_list.append(msg)  98 
 99         while msg_list: 100             print("未完成續傳的數量: %s個".center(150, '-') % len(msg_list)) 101             for msg in msg_list: 102                 print('序號: %s' % (int(msg_list.index(msg) + 1))) 103                 print(msg) 104 
105             choice = input('[退出: q/Q]請根據序號選擇您是否繼續下載沒有完成的文件>>:').strip() 106             if choice.lower() == 'q': 107                 break
108             if not choice.isdigit(): 109                 continue
110             choice = int(choice) 111             if 0 < choice <= len(unfinished_path_list):  # len(unfinished_path_list)=3
112                 dic = unfinished_path_list[choice - 1] 113                 path, unfinished_file_size = dic['path'], dic['unfinished_file_size'] 114 
115                 print('開始續傳......') 116                 self.__resume_download(path, unfinished_file_size) 117                 print('\n續傳完畢!') 118 
119                 unfinished_path_list.pop(choice-1) 120                 msg_list.pop(choice-1) 121             else: 122                 print('您的選擇超出了範圍!') 123 
124     def auth(self): 125         """
126  登錄. 127  100: '用戶名密碼正確, 認證成功!', 128  199: '用戶名密碼不正確, 認證失敗!', 129  850: '您的還有爲上傳完的文件, 是否繼續上傳!', 130  851: '檢測您不存在未上傳完成的文件!', 131         """
132         count = 0 133         while count < 3: 134             username = input('username>>:').strip() 135             password = input('password>>:').strip() 136             if not all([username, password]): 137                 print('用戶名密碼不能爲空.') 138                 count += 1
139                 continue
140             # 發報頭
141             self.send_header(action_type='auth', username=username, password=password) 142             # 收報頭
143             response_dic = self.receive_header() 144             status_code, status_msg = response_dic.get('status_code'), response_dic.get('status_msg') 145             # 100: '用戶名密碼正確, 認證成功!',
146             if status_code == 100:  # 100確認成功
147                 print(status_msg) 148                 self.username = username 149 
150                 # 850: '您的還有爲上傳完的文件, 是否繼續上傳!',
151                 # 851: '檢測您不存在未上傳完成的文件!',
152                 response_dic = self.receive_header() 153                 status_code, status_msg, msg_list, msg_dic = response_dic.get('status_code'), response_dic.get( 154                     'status_msg'), response_dic.get('msg_list'), response_dic.get('msg_dic') 155                 if msg_list: 156                     print(status_msg) 157                     for unfinished_msg in msg_list: 158                         print(unfinished_msg) 159                 else: 160                     print(status_msg) 161 
162                 return True 163             else: 164                 # 199: '用戶名密碼不正確, 認證失敗!',
165                 print(status_msg) 166                 count += 1
167         else: 168             print('輸入次數過多,強制退出!') 169             return False 170 
171     def _ls(self, path): 172         """
173  顯示目錄的文件列表. 174  :param path: [] 或 ['目錄1', '目錄2'] 175  :return: None 176         """
177         # 發送報頭
178         self.send_header(action_type='ls', path=path) 179         # 接收報頭
180         response_dic = self.receive_header() 181         status_code, status_msg, cmd_size = response_dic.get('status_code'), response_dic.get( 182             'status_msg'), response_dic.get('cmd_size') 183         if status_code == 301 and cmd_size: 184             # print('status_msg:', status_msg)
185             # print('cmd_size:', cmd_size)
186             # 收消息
187             windows_cmd = self.socket.recv(cmd_size).decode(self.windows_encoding) 188             print(windows_cmd) 189         else: 190             print(status_msg) 191 
192     def _cd(self, path): 193         """
194  切換目錄. 195  :param path: ['..'] 或 ['路徑1', '目錄2'] 196  :return: None 197         """
198         # 發送報頭
199         self.send_header(action_type='cd', path=path) 200         # 接收報頭
201         response_dic = self.receive_header() 202         status_code, status_msg, current_dir = response_dic.get('status_code'), response_dic.get( 203             'status_msg'), response_dic.get('current_dir') 204         if status_code == 400: 205             self.current_dir = current_dir 206             print(status_msg) 207         else: 208             print(status_msg) 209 
210     def _mkdir(self, path): 211         """
212  新建目錄. 213  :param path: ['目錄1'] 214  或 [目錄2', '目錄3'] 215  :return: None 216         """
217         # print(path)
218         # 發送報頭
219         self.send_header(action_type='mkdir', path=path) 220         # 接收報頭
221         response_dic = self.receive_header() 222         status_code, status_msg = response_dic.get('status_code'), response_dic.get( 223             'status_msg') 224         if status_code == 500: 225             print(status_msg) 226         else: 227             print(status_msg) 228 
229     def _rmdir(self, path): 230         """
231  刪除空目錄. 232  :param path: ['', '12312都1的發'] 233  :return: None 234         """
235         # print(path)
236         # 發送報頭
237         self.send_header(action_type='rmdir', path=path) 238         # 接收報頭
239         response_dic = self.receive_header() 240         status_code, status_msg = response_dic.get('status_code'), response_dic.get( 241             'status_msg') 242         if status_code == 600: 243             print(status_msg) 244         else: 245             print(status_msg) 246 
247     def _remove(self, path): 248         """
249  刪除文件. 250  :param path: ['目錄1', '文件1'] 251  :return: 252         """
253         # print(path)
254         # 發送報頭
255         self.send_header(action_type='remove', path=path) 256         # 接收報頭
257         response_dic = self.receive_header() 258         status_code, status_msg = response_dic.get('status_code'), response_dic.get( 259             'status_msg') 260         if status_code == 700: 261             print(status_msg) 262         else: 263             print(status_msg) 264 
265     def parser_path(self, action_type, path, **kwargs): 266         """
267  解析路徑參數, 判斷路徑是文件名, 仍是路徑下的文件名. 268  :param action_type: 用戶上傳的功能類型 269  :param path: 用戶路徑例子: ['目錄1', '文件1'] 或 ['文件1'] 270  :param kwargs: 271  :return: path列表長度合理的時候返回True, 不合理返回False 272         """
273         if len(path) > 1: 274             self.send_header(action_type=action_type, **kwargs, file_name=path[-1], 275                              path=path[:-1]) 276         elif len(path) == 1: 277             self.send_header(action_type=action_type, **kwargs, file_name=path[-1], 278                              path=None) 279         else: 280             print('必須指定路徑, 或者文件名!') 281             return False 282         return True 283 
284     def _resume_upload(self, path): 285         """
286  upload的斷點續傳功能. 287  860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!', 288  869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!', 289         """
290         self._upload(path, resume_upload=True) 291 
292     def _upload(self, path, resume_upload=False): 293         """
294  上傳文件到服務端. 295  正常上傳: 296  800: '你能夠上傳文件, 在您上傳以前, 您的目前空間:%s!', 297  801: '上傳文件成功, 您上傳完後的剩餘空間:%s!', 298  852: '您不能進行續傳, 由於該文件是完整文件!', 299  894: '您不須要再本路徑下上傳文件, 該文件在您的當前路徑下已經存在!', 300  895: '上傳文件失敗, md5效驗不一致, 部分文件內容在網絡中丟失, 請從新上傳!', 301  896: '上傳文件失敗, 您的空間不足, 您的上傳虛假文件大小, 您的剩餘空間:%s!', 302  897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!', 303  898: '上傳文件失敗, 上傳命令不規範!', 304  899: '上傳文件必需要有文件的md5值以及文件名!', 305  續傳: 306  860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!', 307  869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!', 308  :param path: ['目錄1', '文件1'] 或 ['文件1'] 309  :return: None 310         """
311         # 判斷用戶文件路徑是不是FILES_PATH路徑下的文件
312         file_path = os.path.normpath(os.path.join(settings.FILES_PATH, *path)) 313         if not os.path.isfile(file_path): 314             print('您要上傳的文件不存在!') 315             return
316 
317         # 解析用戶路徑, 並提交upload的相關功能
318         file_size = os.path.getsize(file_path) 319         file_md5 = self.md5(file_path) 320 
321         if resume_upload:  # 斷點續傳時執行
322             action_type = 'resume_upload'
323         else:  # 正常長傳時執行
324             action_type = 'upload'
325 
326         if not self.parser_path(action_type=action_type, file_md5=file_md5, file_size=file_size, path=path): 327             return
328 
329         # 接收服務端相應字典
330         # 正常: 800, 894, 897, 898, 899
331         # 800: '你能夠上傳文件, 在您上傳以前, 您的目前空間:%s!',
332         # 894: '您不須要再本路徑下上傳文件, 該文件在您的當前路徑下已經存在!',
333         # 898: '上傳文件失敗, 上傳命令不規範!',
334         # 897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!',
335         # 899: '上傳文件必需要有文件的md5值以及文件名!',
336         # 續傳: 860, 869
337         # 860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!',
338         # 869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!',
339         response_dic = self.receive_header() 340         status_code, status_msg, residual_space_size, already_upload_size = response_dic.get( 341             'status_code'), response_dic.get( 342             'status_msg'), response_dic.get('residual_space_size'), response_dic.get('already_upload_size') 343 
344         # 判斷狀態碼
345         # 800: '你能夠上傳文件, 在您上傳以前, 您的目前空間:%s!',
346         # 860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!',
347         if status_code == 800 or status_code == 860:  # 800正常發送文件確認 860續傳文件確認
348             print(status_msg % self.conversion_quota(residual_space_size)) 349 
350             initial_size = 0 351             if resume_upload:  # 斷點續傳時執行: 目前文件總大小要減去上次沒有上傳完位置的大小
352                 total_size = file_size - already_upload_size 353             else:  # 正常上傳時執行
354                 total_size = file_size 355             with open(file_path, 'rb') as f: 356                 if resume_upload:  # 斷點續傳時執行: 光標移動到上次沒有上傳完位置
357  f.seek(already_upload_size) 358                 print('\nupload running...') 359                 for line in f: 360  self.socket.sendall(line) 361                     initial_size += len(line) 362                     percent = initial_size / total_size 363  self.progress_bar(percent) 364                 print('\nupload succeed!') 365 
366             # 第二次接收消息, 確認文件上傳完畢
367             # 801, 895, 896
368             # 801: '上傳文件成功, 您上傳完後的剩餘空間:%s!',
369             # 895: '上傳文件失敗, md5效驗不一致, 部分文件內容在網絡中丟失, 請從新上傳!',
370             # 896: '上傳文件失敗, 您的空間不足, 您的上傳虛假文件大小, 您的剩餘空間:%s!',
371             response_dic = self.receive_header() 372             status_code, status_msg, residual_space_size = response_dic.get('status_code'), response_dic.get( 373                 'status_msg'), response_dic.get('residual_space_size') 374             if residual_space_size:  # 801, 896
375                 print(status_msg % self.conversion_quota(residual_space_size)) 376             else:  # 895
377                 print(status_msg) 378         else: 379             # 正常: 894, 897, 898, 899
380             # 894: '您不須要再本路徑下上傳文件, 該文件在您的當前路徑下已經存在!',
381             # 897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!',
382             # 898: '上傳文件失敗, 上傳命令不規範!',
383             # 899: '上傳文件必需要有文件的md5值以及文件名!',
384             # 續傳:
385             # 869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!',
386             if residual_space_size:  # 897
387                 print(status_msg % self.conversion_quota(residual_space_size)) 388             else:  # 869, 894, 898, 899
389                 print(status_msg) 390 
391     def __resume_download(self, path, unfinished_file_size): 392         self._download(path, unfinished_file_size, resume_download=True) 393 
394     def _download(self, path, unfinished_file_size=None, resume_download=False): 395         """
396 
397  900: '準備開始下載文件!', 398  999: '下載文件失敗, 您要下載的文件路徑不規範!', 399  :param path: 400  :param resume_download: 401  :return: 402         """
403         if resume_download: 404             action_type = 'resume_download'
405         else: 406             action_type = 'download'
407         self.send_header(action_type=action_type, path=path, unfinished_file_size=unfinished_file_size) 408 
409         # 接收服務端消息
410         # self.send_header(status_code=900, file_name=file_name, file_size=file_size, file_md5=file_md5)
411         response_dic = self.receive_header() 412         status_code, status_msg, file_name, file_size, file_md5 = response_dic.get('status_code'), response_dic.get( 413             'status_msg'), response_dic.get('file_name'), response_dic.get('file_size'), response_dic.get('file_md5') 414 
415         # 判斷狀態碼
416         # 900: '準備開始下載文件!',
417         # 950: '準備開始續傳文件!',
418         # 998: '下載文件失敗, 您要下載的文件路徑不存在!',
419         # 999: '下載文件失敗, 您要下載的文件路徑不規範!',
420         if status_code == 900 or status_code == 950: 421 
422             file_path = os.path.join(settings.FILES_PATH, file_name) 423             if resume_download and file_path in self.breakpoint_resume.keys(): 424                 unfinished_file_path = self.breakpoint_resume[file_path]['unfinished_file_path'] 425             else: 426                 # 判斷本次路徑下是否有文件, 有文件則提示
427                 # file_path = os.path.join(settings.FILES_PATH, file_name)
428                 if os.path.isfile(file_path): 429                     print('本次路徑下文件已經存在, 不須要繼續下載!') 430                     return
431                 # 爲沒有下載完畢的文件名添加後綴
432                 unfinished_file_path = '%s.%s' % (file_path, 'download') 433 
434             # 爲出現下載終端添加斷點記錄
435             self.breakpoint_resume[unfinished_file_path] = {'file_name': file_name, 'file_size': file_size, 436                                                             'path': path} 437 
438             # 開始進行下載
439             receive_size = 0 440             if resume_download: 441                 total_size = file_size - os.path.getsize(unfinished_file_path) 442                 mode = 'a'
443             else: 444                 total_size = file_size 445                 mode = 'w'
446             with open(unfinished_file_path, '%sb' % mode) as f: 447                 print('\ndownload run...') 448                 while receive_size < total_size: 449                     data_bytes = self.socket.recv(self.max_packet_size) 450  f.write(data_bytes) 451                     receive_size += len(data_bytes) 452                     percent = receive_size / total_size 453  self.progress_bar(percent) 454                 print('\ndownload succeed!') 455  f.flush() 456 
457             # 正常下載成功把後綴去除, 文件更名, 刪除斷點記錄
458             del self.breakpoint_resume[unfinished_file_path] 459  os.rename(unfinished_file_path, file_path) 460 
461             # 效驗md5值詢問用戶是否保存
462             server_file_md5 = file_md5 463             current_file_md5 = self.md5(file_path) 464             if server_file_md5 != current_file_md5: 465                 print('您的文件不完成, 可能不能打開, 請從新下載!') 466         else: 467             # 998: '下載文件失敗, 您要下載的文件路徑不存在!',
468             # 999: '下載文件失敗, 您要下載的文件路徑不規範!',
469             print(status_msg) 470 
471  @staticmethod 472     def conversion_quota(residual_space_size): 473         """
474  換算服務端發送過來的字節爲MB, 人性化的展示用戶的空間剩餘. 475  :param residual_space_size: 剩餘空間字節數 476  :return: MB爲單位的字節 477         """
478         residual_space_mb = residual_space_size / (1024 ** 2) 479         return '%.2fMB' % residual_space_mb 480 
481     def receive_header(self): 482         """
483  接收服務端發送過來的報頭字典. 484  :return: {'status_code': 100, 'status_msg': '認證成功', 'cmd_size': 199} 485         """
486         header_bytes = self.socket.recv(self.fixed_packet_size) 487         header_dic_json_length = struct.unpack(self.struct_fmt, header_bytes)[0] 488         # 接收報頭
489         header_dic_json = self.socket.recv(header_dic_json_length).decode(self.encoding) 490         header_dic = json.loads(header_dic_json) 491         return header_dic 492 
493     def send_header(self, *, action_type, **kwargs): 494         """
495  發送報頭字典給客戶端. 496  :param action_type: action_type='auth' 497  :param kwargs: {'username': 'egon', 'password': '123'} 498  :return: None 499         """
500         request_dic = kwargs 501         request_dic['action_type'] = action_type 502  request_dic.update(request_dic) 503 
504         request_dic_json_bytes = json.dumps(request_dic).encode(self.encoding) 505         request_dic_json_bytes_length = len(request_dic_json_bytes) 506         header_bytes = struct.pack(self.struct_fmt, request_dic_json_bytes_length) 507 
508         # 發送報頭
509  self.socket.sendall(header_bytes) 510         # 發送json後bytes後的字典request_dic
511  self.socket.sendall(request_dic_json_bytes) 512 
513  @staticmethod 514     def md5(file_path): 515         """
516  md5加密哈希文件. 517  :param file_path: files下的文件路徑 518  :return: 文件hash值 519         """
520         md5_obj = hashlib.md5() 521         with open(file_path, 'rb') as f: 522             for line in f: 523  md5_obj.update(line) 524         return md5_obj.hexdigest() 525 
526  @staticmethod 527     def progress_bar(percent, width=50, symbol='#'): 528         """進度條功能."""
529         if percent > 1: 530             percent = 1
531         show_str = ('[%%-%ds]' % width) % (int(width * percent) * symbol) 532         print('\r%s %.2f%%' % (show_str, percent * 100), end='') 533 
534  @staticmethod 535     def show_str(): 536         """顯示客戶端flies中的文件列表."""
537         print('\n------您的files文件夾下所含有的文件------') 538         for index, filename in enumerate(os.listdir(settings.FILES_PATH), 1): 539             print('%s: %s' % (index, filename)) 540         print() 541 
542  @staticmethod 543     def help_msg(msgs=None): 544         """幫助信息."""
545         if msgs in settings.help_dic: 546             print(settings.help_dic[msgs]) 547         else: 548             return True
main.py

3) files

  • 存放上傳服務器的目錄
 1     # encoding: utf-8
 2 
 3 import os  4 import sys  5 
 6 BASE_DIR = os.path.normpath(os.path.join(__file__, '..'))  7 print(BASE_DIR)  8 sys.path.append(BASE_DIR)  9 
10 if __name__ == '__main__': 11     from core import main 12     client = main.FTPClient(('127.0.0.1', 8080)) 13     client.interactive()
ftp_client.py

 

 

②server

1) conf

 1 [egon]  2 password = 202cb962ac59075b964b07152d234b70  3 quota = 100
 4 
 5 [alex]  6 password = 202cb962ac59075b964b07152d234b70  7 quota = 100
 8 
 9 [ly] 10 password = 202cb962ac59075b964b07152d234b70 11 quota = 200
12 
13 [jzd] 14 password = 202cb962ac59075b964b07152d234b70 15 quota = 300
16 
17 [shx] 18 password = 202cb962ac59075b964b07152d234b70 19 quota = 300
20 
21 
22 [xxx] 23 password = 202cb962ac59075b964b07152d234b70 24 quota = 300
accounts.ini
 1 import os  2 
 3 
 4 def base_dir(*args):  5     return os.path.normpath(os.path.join(__file__, '..', '..', *args))  6 
 7 
 8 # 用戶家目錄存放路徑
 9 USER_HOME_DIR = base_dir('home')  10 
 11 # 用戶帳戶信息文件路徑
 12 ACCOUNTS_FILE = base_dir('conf', 'accounts.ini')  13 
 14 # 本機測試的ip和port
 15 HOST = '127.0.0.1'
 16 PORT = 8080
 17 
 18 # 狀態碼: 負責提供交互成功及失敗的提示信息反饋
 19 STATUS_CODE = {  20     100: '用戶名密碼正確, 認證成功!',  21     199: '用戶名密碼不正確, 認證失敗!',  22     200: '您的功能指定不能爲空!',  23     201: '沒有該功能, 請查看幫助信息!',  24     301: '本次返回結果包含命令大小.',  25     400: '切換目錄成功',  26     498: '切換目錄失敗, 切換命令不規範',  27     499: '切換目錄失敗, 目標地址不存在!',  28     500: '建立目錄成功!',  29     598: '建立目錄命令輸入不規範!',  30     599: '建立的目錄已存在!',  31     600: '刪除目錄成功!',  32     699: '刪除目錄失敗, 該目錄不爲空!',  33     698: '刪除目錄失敗, 不存在該目錄!',  34     697: '刪除目錄失敗, 刪除命令不規範!',  35     700: '刪除文件成功!',  36     799: '刪除文件失敗, 不存在該文件!',  37     800: '你能夠上傳文件, 在您上傳以前, 您的目前空間:%s!',  38     801: '上傳文件成功, 您上傳完後的剩餘空間:%s!',  39     850: '服務端檢測您還有爲上傳完的文件, 是否繼續上傳!',  40     851: '服務端檢測您沒有未上傳完成的文件!',  41     852: '您不能進行續傳, 由於該文件是完整文件!',  42     860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!',  43     869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!',  44     894: '您不須要再對本路徑下上傳文件, 該文件在您的當前路徑下已經存在!',  45     895: '上傳文件失敗, md5效驗不一致, 部分文件內容在網絡中丟失, 請從新上傳!',  46     896: '上傳文件失敗, 您的空間不足, 您的上傳虛假文件大小, 您的剩餘空間:%s!',  47     897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!',  48     898: '上傳文件失敗, 上傳命令不規範!',  49     899: '上傳文件必需要有文件的md5值以及文件名!',  50     900: '準備開始下載文件!',  51     950: '準備開始續傳文件!',  52     998: '下載文件失敗, 您要下載的文件路徑不存在!',  53     999: '下載文件失敗, 您要下載的文件路徑不規範!',  54 }  55 
 56 # log日誌路徑
 57 ACCESS_LOG_PATH = base_dir('log', 'access.log')  58 
 59 # 定義log日誌輸出格式
 60 standard_format = '%(asctime)s - %(threadName)s:%(thread)d - task_id:%(name)s - %(filename)s:%(lineno)d - ' \  61                   '%(levelname)s - %(message)s'  # 其中name爲getlogger指定的名字
 62 
 63 simple_format = '\n%(levelname)s - %(asctime)s - %(filename)s:%(lineno)d - %(message)s\n'
 64 
 65 
 66 # log配置字典
 67 LOGGING_DIC = {  68     'version': 1,  69     'disable_existing_loggers': False,  70     'formatters': {  71         'standard': {  72             'format': standard_format  73  },  74         'simple': {  75             'format': simple_format,  76  },  77  },  78     'filters': {},  79     'handlers': {  80         # 打印到終端的日誌
 81         'console': {  82             'level': 'DEBUG',  83             'class': 'logging.StreamHandler',  # 打印到屏幕
 84             'formatter': 'simple'
 85  },  86         # 打印到文件的日誌,收集info及以上的日誌
 87         'access': {  88             'level': 'DEBUG',  89             'class': 'logging.handlers.RotatingFileHandler',  # 保存到文件
 90             'formatter': 'standard',  91             'filename': ACCESS_LOG_PATH,  # 日誌文件
 92             # 'maxBytes': 1024 * 1024 * 5, # 日誌大小 5M
 93             'maxBytes': 1024 * 1024 * 5,  94             'backupCount': 10,  95             'encoding': 'utf-8',  # 日誌文件的編碼,不再用擔憂中文log亂碼了
 96  },  97  },  98     'loggers': {  99         # logging.getLogger(__name__)拿到的logger配置
100         '': { 101             'handlers': ['access', 'console'],  # 這裏把上面定義的兩個handler都加上,即log數據既寫入文件又打印到屏幕
102             'level': 'DEBUG', 103             'propagate': False,  # 向上(更高level的logger)傳遞
104  }, 105  }, 106 }
settings.py

2) core

 1 import json  2 import os  3 import shelve  4 import struct  5 import subprocess  6 
 7 from conf import settings  8 from lib import common  9 
 10 
 11 class HandlerRequest:  12     """處理用戶請求."""
 13     max_packet_size = 8192
 14     encoding = 'utf-8'
 15 
 16     struct_fmt = 'i'
 17     fixed_packet_size = 4
 18 
 19     logger = common.load_my_logging_cfg()  20 
 21     def __init__(self, request, address):  22         self.request = request  23         self.address = address  24 
 25         self.residual_space_size = None  26 
 27         self.breakpoint_resume = None  28 
 29         self.username = None  30         self.user_obj = None  31         self.user_current_dir = None  32 
 33     def client_close(self):  34         """關閉客戶端鏈接."""
 35  self.request.close()  36 
 37     def handle_request(self):  38         """處理客戶端請求."""
 39         count = 0  40         while count < 3:  # 鏈接循環
 41             try:  42                 if self.auth():  43                     # 收消息
 44                     user_dic = self.receive_header()  45                     action_type = user_dic.get('action_type')  46                     if action_type:  47                         if hasattr(self, '_%s' % action_type):  48                             func = getattr(self, '_%s' % action_type)  49  func(user_dic)  50                         else:  51                             self.send_header(status_code=201)  52                     # 發消息
 53                     else:  54                         self.send_header(status_code=200)  55                 else:  56                     count += 1
 57                     self.send_header(status_code=199)  58             except ConnectionResetError:  59                 break
 60         # 關閉客戶端鏈接
 61         self.logger.info('----鏈接斷開---- ip:%s port:%s' % self.address)  62  self.client_close()  63 
 64     def unfinished_file_check(self):  65         self.logger.info('#執行unfinished_file_check命令# ip:%s port:%s' % self.address)  66 
 67         if not list(self.breakpoint_resume.keys()):  68             self.send_header(status_code=851)  69             return
 70 
 71         # self.breakpoint_resume[file_path] =
 72         # {'file_size': _file_size, 'unfinished_file_path': unfinished_file_path, 'file_name': _file_name}
 73         msg_list = []  74 
 75         for index, abs_path in enumerate(self.breakpoint_resume.keys(), 1):\  76 
 77             user_path = '/'.join(abs_path.split(self.username)[-1].split(os.sep))  78             print('abs_path:', user_path)  79             file_name = self.breakpoint_resume[abs_path]['file_name']  80             src_file_size = self.breakpoint_resume[abs_path]['file_size']  81             unfinished_file_size = os.path.getsize(self.breakpoint_resume[abs_path]['unfinished_file_path'])  82             percent = unfinished_file_size / src_file_size * 100
 83 
 84             msg = """
 85  數量: %s 文件路徑: %s 文件名: %s  86  文件原大小: %s字節 未完成的文件大小: %s字節 上傳的百分比: %.2f%%  87             """ % (index, user_path, file_name, src_file_size, unfinished_file_size, percent)  88 
 89  msg_list.append(msg)  90             # msg_dic['/03_函數調用的三種形式.mp4'] = 5772100
 91             # msg_dic[user_path] = unfinished_file_size
 92         # self.send_header(status_code=850, msg_list=msg_list, msg_dic=msg_dic)
 93         self.send_header(status_code=850, msg_list=msg_list)  94 
 95     def auth(self):  96         """用戶登錄認證."""
 97         if self.user_current_dir:  98             return True  99 
100         # 涉及到交叉導入
101         from core import main 102         # 收消息
103         auth_dic = self.receive_header() 104 
105         user_name = auth_dic.get('username') 106         user_password = auth_dic.get('password') 107         md5_password = common.md5('password', password=user_password) 108 
109         # print(user_name, user_password, md5_password)
110 
111         accounts = main.FTPServer.load_accounts() 112         if user_name in accounts.sections(): 113             if md5_password == accounts[user_name]['password']: 114                 self.send_header(status_code=100) 115 
116                 self.username = user_name 117                 self.user_obj = accounts[user_name] 118                 self.user_obj['home'] = os.path.join(settings.USER_HOME_DIR, user_name) 119                 self.user_current_dir = self.user_obj['home'] 120 
121                 # print('self.user_obj:', self.user_obj)
122                 # print("self.user_obj['home']:", self.user_obj['home'])
123 
124                 self.residual_space_size = common.conversion_quota( 125                     self.user_obj['quota']) - common.get_size(self.user_obj['home']) 126 
127                 breakpoint_resume_dir_path = os.path.join(self.user_obj['home'], '.upload') 128                 if not os.path.isdir(breakpoint_resume_dir_path): 129  os.mkdir(breakpoint_resume_dir_path) 130                 self.breakpoint_resume = shelve.open(os.path.join(breakpoint_resume_dir_path, '.upload.shv')) 131  self.unfinished_file_check() 132 
133                 self.logger.info('#認證成功# ip:%s port:%s' % self.address) 134                 return True 135         self.logger.info('#認證失敗# ip:%s port:%s' % self.address) 136         return False 137 
138     def _ls(self, cmd_dic): 139         """
140  運行dir命令將結果發送到客戶端. 141  :param cmd_dic: {'path': [], 'action_type': 'ls'} 142  或 {'path': ['目錄1', '目錄2', '目錄xxx'], 'action_type': 'ls'} 143  或 {'path': ['?'], 'action_type': 'ls'} 144  :return: None 145         """
146         # print('_ls:', cmd_dic)
147         self.logger.info('#執行ls命令# ip:%s port:%s' % self.address) 148 
149         # 覈驗路徑
150         dir_path = self.verify_path(cmd_dic) 151         if not dir_path: 152             dir_path = self.user_current_dir 153 
154         if cmd_dic.get('path') == ['?']:  # 爲用戶提供ls /?命令
155             dir_path = '/?'
156 
157         sub_obj = subprocess.Popen( 158             'dir %s' % dir_path, 159             shell=True, 160             stderr=subprocess.PIPE, 161             stdout=subprocess.PIPE 162  ) 163         stderr_bytes, stdout_bytes = sub_obj.stderr.read(), sub_obj.stdout.read() 164         cmd_size = len(stderr_bytes) + len(stdout_bytes) 165 
166         # 發報頭
167         self.send_header(status_code=301, cmd_size=cmd_size) 168         # 發消息
169  self.request.sendall(stderr_bytes) 170  self.request.sendall(stdout_bytes) 171 
172     def _cd(self, cmd_dic): 173         """
174  根據用戶的目標目錄, 改變用戶的當前目錄的值. 175  :param cmd_dic: {'action_type': 'cd', 'path': ['..']} 176  或 {'action_type': 'cd', 'path': ['目錄1', '目錄2', '目錄xxx'], } 177  :return: None 178  Z:\\pycharm\\開發FTP程序之路\\第2次FTP_第四模塊做業\\FUCK_FTP\\server\\home\\egon\\目錄1 179         """
180         # print('_cd:', cmd_dic)
181         self.logger.info('#執行cd命令# ip:%s port:%s' % self.address) 182 
183         # 覈驗路徑
184         dir_path = self.verify_path(cmd_dic) 185         if dir_path: 186             if os.path.isdir(dir_path):  # 判斷用戶切換的路徑是否存在
187                 self.user_current_dir = dir_path 188                 if dir_path == self.user_obj['home']: 189                     current_dir = '~'
190                 else: 191                     join_dir = ''.join(dir_path.split('%s' % self.username)[1:]) 192                     current_dir = '/'.join(join_dir.split('\\')) 193                 self.send_header(status_code=400, current_dir=current_dir) 194             else: 195                 self.send_header(status_code=499) 196         else: 197             self.send_header(status_code=498) 198 
199     def _mkdir(self, cmd_dic): 200         """
201  更具用戶的目標目錄, 且目錄不存在, 建立目錄標目錄, 生成多層遞歸目錄. 202  :param cmd_dic: {'action_type': 'mkdir', 'path': ['目錄1']} 203  或 {'action_type': 'mkdir', 'path': ['目錄2', '目錄3', '目錄xxx']} 204  :return: None 205         """
206         # print('_mkdir:', cmd_dic)
207         self.logger.info('#執行mkdir命令# ip:%s port:%s' % self.address) 208 
209         dir_path = self.verify_path(cmd_dic) 210         if dir_path: 211             if not os.path.isdir(dir_path):  # 判斷用戶要建立的目錄時否存在
212  os.makedirs(dir_path) 213                 self.send_header(status_code=500) 214             else: 215                 self.send_header(status_code=599) 216         else: 217             self.send_header(status_code=598) 218 
219     def _rmdir(self, cmd_dic): 220         """
221  更具用戶的目標目錄, 刪除不爲空的目錄. 222  :param cmd_dic: {'path': ['目錄1', '目錄xxx', '空目錄'], 'action_type': 'rmdir'} 223  :return: None 224         """
225         # print('_rmdir:', cmd_dic)
226         self.logger.info('#執行rmdir命令# ip:%s port:%s' % self.address) 227 
228         dir_path = self.verify_path(cmd_dic) 229         if dir_path: 230             if os.path.isdir(dir_path): 231                 if os.listdir(dir_path): 232                     self.send_header(status_code=699) 233                 else: 234  os.rmdir(dir_path) 235                     self.send_header(status_code=600) 236             else: 237                 self.send_header(status_code=698) 238         else: 239             self.send_header(status_code=697) 240 
241     def _remove(self, cmd_dic): 242         """
243  更具用戶的目標文件, 刪除該文件 244  :param cmd_dic: {'path': ['目錄1', '目錄xxx', '文件'], 'action_type': 'remove'} 245  :return: 246         """
247         # print('_remove:', cmd_dic)
248         self.logger.info('#執行remove命令# ip:%s port:%s' % self.address) 249         file_path = self.verify_path(cmd_dic) 250 
251         if file_path: 252             if os.path.isfile(file_path): 253                 # 判斷用戶刪除的文件是不是要續傳的文件, 若是是則先把把續傳的記錄刪除
254                 if file_path in self.breakpoint_resume.keys: 255                     del self.breakpoint_resume[file_path] 256  os.remove(file_path) 257                 self.send_header(status_code=700) 258             else: 259                 self.send_header(status_code=799) 260         else: 261             self.send_header(status_code=798) 262 
263     def _resume_upload(self, cmd_dic): 264         """
265  860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!', 266  869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!', 267  :param cmd_dic: 268  :return: 269         """
270         # print('def _resume_upload ===> cmd_args', cmd_dic)
271         self.logger.info('#執行resume_upload命令# ip:%s port:%s' % self.address) 272         self._upload(cmd_dic, resume_upload=True) 273 
274     def _upload(self, cmd_dic, resume_upload=False): 275         """客戶端 276  800: '你能夠上傳文件, 在您上傳以前, 您的目前空間:%s!', 277  801: '上傳文件成功, 您上傳完後的剩餘空間:%s!', 278  850: '您的還有爲上傳完的文件, 是否繼續上傳!', 279  851: '檢測您不存在未上傳完成的文件!', 280  852: '您不能進行續傳, 由於該文件是完整文件!', 281  860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!', 282  869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!', 283  894: '您不須要再對本路徑下上傳文件, 該文件在您的當前路徑下已經存在!', 284  895: '上傳文件失敗, md5效驗不一致, 部分文件內容在網絡中丟失, 請從新上傳!', 285  896: '上傳文件失敗, 您的空間不足, 您的上傳虛假文件大小, 您的剩餘空間:%s!', 286  897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!', 287  898: '上傳文件失敗, 上傳命令不規範!', 288  899: '上傳文件必需要有文件的md5值以及文件名!', 289         """
290         # print('_upload:', cmd_dic)
291         if not resume_upload: 292             self.logger.info('#執行upload命令# ip:%s port:%s' % self.address) 293 
294         # 效驗: 897, 898, 899
295         _path, _file_md5, _file_name, _file_size = cmd_dic.get('path'), cmd_dic.get('file_md5'), cmd_dic.get( 296             'file_name'), cmd_dic.get('file_size') 297         file_path = self.verify_upload_action(cmd_dic, _path=_path, _file_md5=_file_md5, _file_name=_file_name, 298 
299                                               _file_size=_file_size) 300 
301         if resume_upload:   # 斷點續傳時執行
302             if not file_path or file_path not in self.breakpoint_resume.keys(): 303                 # 869: '您選擇文件路徑中沒有要續傳的文件, 請覈對!',
304                 self.send_header(status_code=869) 305                 return
306 
307             # 找到以前未穿完的文件名
308             unfinished_file_path = self.breakpoint_resume[file_path]['unfinished_file_path'] 309             already_upload_size = os.path.getsize(unfinished_file_path) 310 
311             # 效驗成功通知續傳信號
312             # 860: '您正在繼續上傳文件, 在您繼傳以前, 您的目前空間:%s!',
313             self.send_header(status_code=860, residual_space_size=self.residual_space_size, 314                              already_upload_size=already_upload_size) 315 
316             total_size = _file_size - already_upload_size 317             mode = 'a'
318         else:           # 正常上傳執行
319             if not file_path: 320                 return
321 
322             # 判斷用戶上傳的文件是否重複
323             if os.path.isfile(file_path): 324                 # 894: '您不須要再對本路徑下上傳文件, 該文件在您的當前路徑下已經存在!',
325                 self.send_header(status_code=894) 326                 return
327             else: 328                 unfinished_file_path = '%s.%s' % (file_path, 'upload') 329 
330             # 效驗成功通知上傳信號: 800
331             # 800: '你能夠上傳文件, 在您上傳以前, 您的目前空間:%s!',
332             self.send_header(status_code=800, residual_space_size=self.residual_space_size) 333 
334             total_size = _file_size 335             mode = 'w'
336 
337         # 記錄斷點的功能: 在服務端用戶的路徑, 記錄文件大小, 加上後綴的路徑, 文件名
338         # 或再次爲未傳完的文件記錄斷點
339         self.breakpoint_resume[file_path] = {'file_size': _file_size, 'unfinished_file_path': unfinished_file_path, 340                                              'file_name': _file_name} 341 
342         # 開始接收文件
343         receive_size = 0 344         with open(unfinished_file_path, '%sb' % mode) as f: 345             while receive_size < total_size: 346                 data_bytes = self.request.recv(self.max_packet_size) 347                 receive_size += len(data_bytes) 348  f.write(data_bytes) 349         # 接收完畢, 把後綴改爲用戶上傳的文件名
350  os.rename(unfinished_file_path, file_path) 351         # 刪除記錄斷點的功能
352         del self.breakpoint_resume[file_path] 353 
354         # 801, 895, 896
355         # 效驗用戶端發送的md5於本次上傳完畢的md5值
356         upload_file_md5 = common.md5(encryption_type='file', path=file_path) 357         if upload_file_md5 != _file_md5: 358             # print('def _upload ===> upload_file_md5:%s, _file_md5:%s' % (upload_file_md5, _file_md5))
359             # 895: '上傳文件失敗, md5效驗不一致, 部分文件內容在網絡中丟失, 請從新上傳!',
360             self.send_header(status_code=895) 361  os.remove(file_path) 362             return
363 
364         # 安全性問題: 再次判斷用戶是否以假的文件大小來跳出服務端限制的配額
365         if receive_size > self.residual_space_size: 366             # 896: '上傳文件失敗, 您的空間不足, 您的上傳虛假文件大小, 您的剩餘空間:%s!',
367             self.send_header(status_code=896, residual_space_size=self.residual_space_size) 368  os.remove(file_path) 369             return
370         else: 371             self.residual_space_size = self.residual_space_size - receive_size 372             # print('def _upload ===> receive_size:', receive_size)
373             # print('def _upload ===> os.path.getsize(file_path)', os.path.getsize('%s' % file_path))
374             # 801: '上傳文件成功, 您上傳完後的剩餘空間:%s!',
375             self.send_header(status_code=801, residual_space_size=self.residual_space_size) 376 
377     def _resume_download(self, cmd_dic): 378         self._download(cmd_dic, resume_download=True) 379 
380     def _download(self, cmd_dic, resume_download=False): 381         self.logger.info('#執行download命令# ip:%s port:%s' % self.address) 382 
383         file_path = self.verify_path(cmd_dic) 384         if not file_path: 385             # 999: '下載文件失敗, 您要下載的文件路徑不規範!',
386             self.send_header(status_code=999) 387             return
388 
389         if not os.path.isfile(file_path): 390             # 998: '下載文件失敗, 您要下載的文件路徑不存在!',
391             self.send_header(status_code=998) 392             return
393 
394         # 通知能夠開始下載
395         # 900: '準備開始下載文件!'.
396         file_name = file_path.split(os.sep)[-1] 397         file_size = os.path.getsize(file_path) 398         file_md5 = common.md5('file', file_path) 399         unfinished_file_size = cmd_dic.get('unfinished_file_size') 400         if resume_download: 401             # 950: '準備開始續傳文件!',
402             self.send_header(status_code=950, file_name=file_name, file_size=file_size, file_md5=file_md5) 403         else: 404             # 900: '準備開始下載文件!'.
405             self.send_header(status_code=900, file_name=file_name, file_size=file_size, file_md5=file_md5) 406 
407         # 打開文件發送給客戶端
408         with open(file_path, 'rb') as f: 409             if resume_download: 410  f.seek(unfinished_file_size) 411             for line in f: 412  self.request.sendall(line) 413 
414     def verify_upload_action(self, cmd_dic, *, _path, _file_name, _file_md5, _file_size): 415         """
416  覈驗上傳功能. 417  897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!', 418  898: '上傳文件失敗, 上傳命令不規範!', 419  899: '上傳文件必需要有文件的md5值以及文件名!', 420         """
421         # _path=['03_函數調用的三種形式.mp4']
422         if _path is None: 423             if _file_name and _file_md5 and _file_size: 424                 if _file_size > self.residual_space_size: 425                     # print('def _upload ===> self.residual_space_size:', self.residual_space_size)
426 
427                     # 897: '上傳文件失敗, 您的空間不足, 您的剩餘空間:%s!',
428                     self.send_header(status_code=897, residual_space_size=self.residual_space_size) 429                     return False 430                 else: 431                     # Z:\pycharm\開發FTP程序之路\第2次FTP_第四模塊做業\FUCK_FTP\server\home\egon\03_函數調用的三種形式.mp4
432                     file_path = os.path.join(self.user_current_dir, _file_name) 433             else: 434                 # 899: '上傳文件必需要有文件的md5值以及文件名!',
435                 self.send_header(status_code=899) 436                 return False 437         else: 438             path = self.verify_path(cmd_dic) 439 
440             if not path: 441                 # 898: '上傳文件失敗, 上傳命令不規範!',
442                 self.send_header(status_code=898) 443                 return False 444             else: 445                 # Z:\pycharm\開發FTP程序之路\第2次FTP_第四模塊做業\FUCK_FTP\server\home\egon\03_函數調用的三種形式.mp4
446                 file_path = os.path.join(path, _file_name) 447         return file_path 448 
449     def verify_path(self, cmd_dic): 450         """
451  覈驗客戶端傳過來的路徑. 452  :param cmd_dic: {'action_type': 'ls', 'path': []} 453  或 {'action_type': 'ls', 'path': ['目錄1', '目錄xxx']} 454  或 {action_type': 'cd', 'path': ['目錄2', '目錄xxx']} 455  :return: None 456  Z:\\pycharm\\開發FTP程序之路\\第2次FTP_第四模塊做業\\FUCK_FTP\\server\\home\\egon\\目錄1 457  Z:\\pycharm\\開發FTP程序之路\\第2次FTP_第四模塊做業\\FUCK_FTP\\server\\home\\egon\\目錄1 458         """
459         # print(cmd_dic)
460         path = cmd_dic.get('path') 461         if path: 462             if isinstance(path, list): 463                 for element in path: 464                     if not isinstance(element, str): 465                         path = None 466                         return path 467                 abspath = os.path.normpath(os.path.join(self.user_current_dir, *path)) 468                 # print('def verify_path() ===> abspath:', abspath)
469                 if abspath.startswith(self.user_obj['home']): 470                     path = abspath 471                 else: 472                     path = None  # 用戶目錄超出限制
473             else: 474                 path = None  # 不是列表類型例: '字符串'
475         else: 476             path = None  # []
477         # print('def verify_path() ====> path', path)
478         return path 479 
480     def receive_header(self): 481         """
482  接收客戶端數據. 483  :return: {'action_type': 'cd', 'path': ['目錄1', '目錄xxx']} 484         """
485         header_bytes = self.request.recv(self.fixed_packet_size) 486         request_dic_json_length = struct.unpack(self.struct_fmt, header_bytes)[0] 487         # print('request_dic_json_length:', request_dic_json_length)
488         # 接收報頭
489         request_dic_json = self.request.recv(request_dic_json_length).decode(self.encoding) 490         request_dic = json.loads(request_dic_json) 491 
492         # print('request_dic:', request_dic)
493 
494         if not request_dic: 495             return {} 496         # print("def receive_header():", request_dic)
497         return request_dic 498 
499     def send_header(self, *, status_code, **kwargs): 500         """
501  發送數據給客戶端. 502  :param status_code: 400 503  :param kwargs: {'current_dir': '/home/egon/目錄1/目錄xxx'} 504  :return: None 505         """
506         # print(status_code)
507         # print(kwargs)
508         from core import main 509 
510         response_dic = kwargs 511         response_dic['status_code'] = status_code 512         response_dic['status_msg'] = main.FTPServer.STATUS_CODE[status_code] 513  response_dic.update(kwargs) 514 
515         response_dic_json_bytes = json.dumps(response_dic).encode(self.encoding) 516         response_dic_json_bytes_length = len(response_dic_json_bytes) 517         header_bytes = struct.pack(self.struct_fmt, response_dic_json_bytes_length) 518 
519         # print('header_bytes:', header_bytes)
520 
521         # 發送報頭
522  self.request.sendall(header_bytes) 523         # 發送json後bytes後的字典response_dic
524         self.request.sendall(response_dic_json_bytes)
handler_request.py
 1 import configparser  2 import socket  3 
 4 from conf import settings  5 from core import handler_request, mythreadpool  6 from lib import common  7 
 8 
 9 class FTPServer: 10     """FTP服務器."""
11     address_family = socket.AF_INET 12     socket_type = socket.SOCK_STREAM 13     allow_reuse_address = False 14     request_queue_size = 5
15 
16     max_pool_size = 5
17 
18     STATUS_CODE = settings.STATUS_CODE 19 
20     logger = common.load_my_logging_cfg() 21 
22     def __init__(self, management_instance, bind_address, bind_and_activate=True): 23         self.management_instance = management_instance 24 
25         self.pool = mythreadpool.MyThreadPool(self.max_pool_size) 26 
27         self.bind_address = bind_address 28         self.socket = socket.socket(self.address_family, self.socket_type) 29 
30         if bind_and_activate: 31             try: 32  self.server_bind() 33  self.server_activate() 34             except Exception: 35  self.server_close() 36                 raise
37 
38     def server_bind(self): 39         """服務器綁定IP,端口."""
40         if self.allow_reuse_address: 41             self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 42  self.socket.bind(self.bind_address) 43 
44     def server_activate(self): 45         """服務器激活."""
46  self.socket.listen(self.request_queue_size) 47 
48     def server_close(self): 49         """關閉服務socket對象."""
50  self.socket.close() 51 
52     def serve_forever(self): 53         """服務器永遠運行."""
54         while True:  # 通訊循環
55             request, address = self.socket.accept() 56 
57             self.logger.info('----鏈接----# ip:%s port:%s' % address) 58 
59             # 來一個鏈接, 實例化一個處理用戶請求的對象
60             handler_response = handler_request.HandlerRequest(request, address) 61             # 來了一個鏈接取走一個線程
62             thread = self.pool.get_thread() 63             # 同時再添加一個線程
64  self.pool.put_thread() 65             t = thread(target=handler_response.handle_request) 66  t.start() 67 
68  @staticmethod 69     def load_accounts(): 70         conf_obj = configparser.ConfigParser() 71  conf_obj.read(settings.ACCOUNTS_FILE) 72         return conf_obj
main.py
 1 import sys  2 
 3 from conf import settings  4 from core import main  5 
 6 
 7 class ManagementTool(object):  8     """管理服務器."""
 9     center_args1, center_args2 = 50, '-'
10 
11     def __init__(self): 12         self.script_argv = sys.argv 13         self.commands = None 14 
15         # print(self.script_argv)
16 
17  self.verify_argv() 18 
19     def verify_argv(self): 20         """
21  覈查參數時否合理. 22  例: 23  ['啓動文件路徑', 'start', 'ftp', 'server'] 24         """
25         if len(self.script_argv) != 4: 26  self.help_msg() 27 
28         action_type = self.script_argv[1] 29         self.commands = self.script_argv[2:] 30         if hasattr(self, action_type): 31             func = getattr(self, action_type) 32  func() 33         else: 34  self.help_msg() 35 
36  @staticmethod 37     def help_msg(): 38         msg = """
39  ------嚴格要求輸入如下命令:------ 40  ① start ftp server 41  ② stop ftp server 42  ③ restart ftp server 43         """
44  exit(msg) 45 
46     def start(self): 47         """啓動ftp服務."""
48         if self.execute(): 49             print('FTP started successfully!') 50             # FTPServer中可能用到ManagementTool中功能
51             server = main.FTPServer(self, (settings.HOST, settings.PORT)) 52  server.serve_forever() 53         else: 54  self.help_msg() 55 
56     def execute(self): 57         """解析命令."""
58         args1, args2 = self.commands 59         if args1 == 'ftp' and args2 == 'server': 60             return True 61         return False
management.py
 1 import os  2 import queue  3 from threading import Thread  4 
 5 
 6 class MyThreadPool:  7     def __init__(self, max_workers=None):  8         if not max_workers:  9             max_workers = os.cpu_count() * 5
10         if max_workers <= 0: 11             raise ValueError('max_workers 必須大於0') 12 
13         self.queue = queue.Queue(max_workers) 14         for count in range(max_workers): 15  self.put_thread() 16 
17     def put_thread(self): 18  self.queue.put(Thread) 19 
20     def get_thread(self): 21         return self.queue.get()
mythreadpool.py

3) home

  • 用戶目錄,以用戶名做爲文件名

4) lib

 1 import hashlib  2 import logging.config  3 import os  4 
 5 from conf import settings  6 
 7 
 8 def md5(encryption_type, path=None, password=None):  9     """
10  md5加密. 11  :param encryption_type: 加密的類型, 支持file和password兩種 12  :param path: 文件或目錄路徑 13  :param password: 明文密碼 14  :return: 加密後的md5值 15     """
16     md5_obj = hashlib.md5() 17     if encryption_type == 'file': 18         if os.path.isfile(path): 19             with open(path, 'rb') as f: 20                 for line in f: 21  md5_obj.update(line) 22             return md5_obj.hexdigest() 23         for filename in os.listdir(path): 24             current_path = os.path.join(path, filename) 25             if os.path.isdir(current_path): 26                 md5(encryption_type, path=current_path) 27             else: 28                 with open(current_path, 'rb') as f: 29                     for line in f: 30  md5_obj.update(line) 31     elif encryption_type == 'password': 32         md5_obj.update(password.encode('utf-8')) 33     return md5_obj.hexdigest() 34 
35 
36 def load_my_logging_cfg(): 37     """
38  加載日誌字典. 39  :return: logger對象 40     """
41  logging.config.dictConfig(settings.LOGGING_DIC) 42     logger = logging.getLogger(__name__) 43     return logger 44 
45 
46 def get_size(path): 47     """
48  遍歷用戶path, 拿到path的路徑大小, 該大小包含目錄下的全部文件. 49  :param path: 路徑 50  :return: 該路徑下的全部文件的大小 51     """
52     initial_size = 0 53     if os.path.isfile(path): 54         return os.path.getsize(path) 55     for filename in os.listdir(path): 56         current_path = os.path.join(path, filename) 57         if os.path.isdir(current_path): 58  get_size(current_path) 59         else: 60             initial_size += os.path.getsize(current_path) 61     return initial_size 62 
63 
64 def conversion_quota(quota_mb: str): 65     """
66  換算用戶磁盤配額, 把MB換算成bytes. 67  :param quota_mb: 68  :return: 知足isdigit返回quota_bytes, 不知足設置默認的配額大小 69     """
70     if quota_mb.isdigit(): 71         quota_mb = int(quota_mb) 72         quota_bytes = quota_mb * 1024 ** 2
73         # print('def conversion_quota ===> quota_bytes:', quota_bytes)
74         return quota_bytes 75     else: 76         default_quota_bytes = 50 * 1024 ** 2
77         return default_quota_bytes
common.py

5) log

 

  •  access.log
 1 # encoding:utf-8
 2 
 3 import os  4 import sys  5 
 6 BASE_DIR = os.path.normpath(os.path.join(__file__, '..'))  7 sys.path.append(BASE_DIR)  8 
 9 if __name__ == '__main__': 10     from core import management 11     management = management.ManagementTool() 12     management.execute()
ftp_server.py
相關文章
相關標籤/搜索