1. 用戶加密認證 2. 容許多用戶登陸 3. 每一個用戶都有本身的家目錄,且只能訪問本身的家目錄 4. 對用戶進行磁盤分配,每個用戶的可用空間能夠本身設置 5. 容許用戶在ftp server上隨意切換目錄 6. 容許用戶查看本身家目錄下的文件 7. 容許用戶上傳和下載,保證文件的一致性(md5) 8. 文件上傳、下載過程當中顯示進度條 9. 支持多併發的功能 10. 使用隊列queue模塊,實現線程池 11. 容許用戶配置最大的併發數,好比容許只有10併發用戶 升級需求:10%
1. 文件支持斷點續傳
python
客戶端: |-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 # 服務端啓動程序
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
1 用戶名: 用戶密碼: 2 alex 123
3 egon 123
4 ly 123
5 jzd 123
6 shx 123
1 username>>:egon 2 password>>:123
3 用戶名密碼正確, 認證成功!
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 .
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 [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 可用字節
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 ~]#
相對路徑切換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 [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的目錄]#
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的目錄/我是江傻子的目錄]#
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 可用字節
相對路徑建立:
mkdir /目錄
生成多層遞歸目錄:
mkdir /目錄1/目錄2
1 [egon@localhost ~]# mkdir /a
2 建立目錄成功!
1 [egon@localhost ~]# mkdir /a/b
2 建立目錄成功!
刪除空目錄:
rmdir /目錄1/空目錄2
1 [egon@localhost ~]# rmdir /a/b
2 刪除目錄成功!
刪除文件
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的目錄 的目錄
上傳到服務端當前路徑:
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 [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 可用字節
繼續上傳文件到服務端當前路徑:
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 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!
從服務端當前目錄下下載文件
download 文件
從服務端絕對路徑下下載文件
download /目錄1/文件
1 [egon@localhost ~]# download 服務器管理綜合報告.docx
2
3 download run... 4 [##################################################] 100.00%
5 download succeed!
1 [egon@localhost ~]# download /我是egon的目錄/03_函數調用的三種形式.mp4
2
3 download run... 4 [##################################################] 100.00%
5 download succeed!
用戶登錄的時候顯示爲下載完的文件
用戶根據序號選擇要繼續續傳的文件
用戶能夠屢次循環選擇
支持斷點之後據續斷點續傳
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
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!
client用戶暫時只能用files文件夾下的路徑進行上傳下載, 不能動態指定
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 }
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
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()
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
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 }
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)
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
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
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()
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
5) 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()