一,項目題目:基於線程開發一個FTP服務器
二,項目要求:
基本要求:
1.用戶加密認證 2.允許同時多用戶登錄 3.每個用戶有自己的家目錄 ,且只能訪問自己的家目錄 4.對用戶進行磁盤配額,每個用戶的可用空間不同 5.允許用戶在ftp server上隨意切換目錄 6.允許用戶查看當前目錄下文件 7.允許上傳和下載文件,保證文件一致性(md5) 8.文件傳輸過程中顯示進度條 9.附加功能:支持文件的斷點續傳
擴展需求:
作業需求: 1.在之前開發的FTP基礎上,開發支持多並發的功能 2.不能使用SocketServer模塊,必須自己實現多線程 3.必須用到隊列Queue模塊,實現線程池 4.允許配置最大並發數,比如允許只有10個並發用戶
三,注意事項:
基本操作:滿足需求1,2,3,4 大神要求:代碼寫的健壯、清晰
四,項目分析:
1,用戶加密認證
這個肯定需要用到configparser 和hashlib模塊,用md5進行加密,服務端與用戶端 進行交互前,肯定需要進行認證,在服務端進行認證,客戶端需要發送用戶名及密碼,但 是為了安全起見,服務端數據庫中的密碼應該是加密后的密文,客戶端登陸認證時也應該 發送密文到服務端,服務端接受到密文與數據庫中對應的密文進行比較。
2,查看自己的當前目錄下的文件
這個只需要寫一個dir就ok 簡單的說,使用configparse模塊就可以完成
3,文件傳輸中顯示進度條
下載的進度條比較好實現,我們可以從服務端受到將要下載的文件的大小, 上傳的進度條,我們可以利用文件操作的tell()方法,獲取當前指針位置(字節)
4,在之前的基礎上實現多並發的功能
並發,是偽並行,,即看起來是同時運行的,單個CPU+多道技術就可以實現並發
多道技術概念回顧:內存中同時存入多道(多個)程序,cpu從一個進程快速切換
到另外一個,使每個進程各自運行幾十或幾百毫秒,這樣,雖然在某一個瞬間,一個cpu
只能執行一個任務,但在1秒內,cpu卻可以運行多個進程,這就給人產生了並行的錯覺,
即偽並發,以此來區分多處理器操作系統的真正硬件並行(多個cpu共享同一個物理內存)
5,不能使用SocketServer模塊,必須自己實現多線程
多線程(即多個控制線程)的概念是,在一個進程中存在多個線程,多個線程共享該進程的地址空間,
相當於一個車間內有多個流水線,都共有一個車間的資源。例如,北京地鐵與上海地鐵是不同的進程,而北京
地鐵里的1號線是一個線程,北京地鐵所有的線路共享該地鐵的所有資源。
為什么使用多線程?
1,同一個進程內可以共享該進程內的地址資源
2,創建線程的開銷遠小於創建進程的開銷(創建一個進程,就是創建一個車間,涉及到申請空間
,而且在該空間內至少創建一個流水線,但是創建線程,就只是在一個車間內造一條流水線,無需申請
空間,所以創建開銷小)
6,必須用到隊列Queue模塊,實現線程池
使用線程池即可,如果不會可以參考此博客的練習題:http://www.cnblogs.com/wj-1314/p/9039970.html
7,允許配置最大並發數,比如允許只有10個並發用戶
8,小編的主要思路
- 1 對於此項目,最初的想法是寫出上傳,和下載文件的程序,包括客戶端和服務端。 - 2 在此基礎上擴展程序,包括提出開始程序到bin里面,配置文件在config里面 - 3 然后把上傳文件和下載文件的程序進行斷點續傳的程序重構 - 4 在此基礎上,對文件進行加密 - 5 增加功能,包括設置進度條,增加查看功能,增加目錄功能,刪除文件功能,切換目錄功能等 - 6 然后再設置磁盤分配功能,完善內容 - 7 然后添加用戶登陸,包括對用戶的密碼加密等功能 - 8 寫完后檢查程序
五,README文件
## 作者:zhanzhengrecheng
## 版本:示例版本 v0.1
## 程序介紹:
- 實現了基於線程開發一個FTP服務器的常用功能
- 基本功能全部用python的基礎知識實現,用到了socket\hashlib\configparse\os\sys\pickle\函數\模塊\類知識
- 在保證了支持多並發的功能上,不使用SocketServer模塊,自己實現了多線程,而且使用了隊列
## 概述
本次作業文件夾一共包含了以下4個文件:
- 程序結構圖:整個Thread_based_FTP_homework的程序文件結構
- 程序結構文件:整個Thread_based_FTP_homework的程序文件結構
- 程序文件: Thread_based_FTP_homework
- 程序說明文件:README.md
## 程序要求
- 1.用戶加密認證
- 2.允許同時多用戶登錄
- 3.每個用戶有自己的家目錄 ,且只能訪問自己的家目錄
- 4.對用戶進行磁盤配額,每個用戶的可用空間不同
- 5.允許用戶在ftp server上隨意切換目錄
- 6.允許用戶查看當前目錄下文件
- 7.允許上傳和下載文件,保證文件一致性(md5)
- 8.文件傳輸過程中顯示進度條
- 9.附加功能:支持文件的斷點續傳
- 10.在之前開發的FTP基礎上,開發支持多並發的功能
- 11.不能使用SocketServer模塊,必須自己實現多線程
- 12.必須用到隊列Queue模塊,實現線程池
- 13.允許配置最大並發數,比如允許只有10個並發用戶
## 本項目思路
- 1 對於此次項目,在上次作業的基礎上完成
- 2 本次首要任務,為了降低程序的耦合性,將把server端和client端的許多東西分出來,保證一個函數只做一件事情
- 3 發現了上次作業里面出現的小問題,進行了解決
- 4 使用隊列Queue模塊,實現多線程
- 5 設置配置最大的並發數,此處設置在settings里面,最大並發用戶設置為3
##### 備注(程序結構)
> 目前還不會把程序樹放在README.md里面,所以做出程序結構的txt版本和圖片版本,放在文件外面方便查看
## 對幾個實例文件的說明
### 幾個實例文件全是為了上傳和下載使用,自己隨便找的素材,沒有把視頻,照片上傳
## 不足及其改進的方面
### 每次程序從用戶登陸到使用只能完成一次功能,不能重復使用
## 程序結構
│ Thread_based_FTP_homework
│ __init__.py
│
├─client # 客戶端程序入口
│ │ __init__.py
│ ├─bin # 可執行程序入口目錄
│ │ run.py
│ │ __init__.py
│ ├─config # 配置文件目錄
│ │ │ settings.py # 配置文件
│ │ │ __init__.py
│ ├─core # 主要邏輯程序目錄
│ │ │ file_func.py # client端文件操作功能模塊
│ │ │ ftp_client.py # client端主程序模塊
│ │ │ md5_func.py # client端對文件加密操作功能模塊
│ │ │ progress_bar_func_func.py # client端文件下載進度條操作功能模塊
│ │ │ __init__.py
│ ├─download # 下載內容模塊
│ │ a.txt
│ │ b.txt
│ │ c.txt
│ └─upload # 上傳內容模塊
│ a.txt
│ b.txt
└─server # 服務端程序入口
├─bin
│ run.py # 可執行程序入口目錄
│ __init__.py
├─config # 配置文件目錄
│ │ accounts.ini # 賬號密碼配置文件
│ │ settings.py # 配置文件
│ │ __init__.py
├─core # 主要邏輯程序目錄
│ │ ftp_server.py # server端主程序模塊
│ │ main.py # 主程序模塊
│ │ user_handle.py # 用戶注冊登錄模塊
│ │ file_func.py # server端文件操作功能模塊
│ │ auth_func.py # server端用戶認證功能模塊
│ │ md5_func.py # server端對文件加密操作功能模塊
└─home # 家目錄
│ __init__.py
├─curry # curry用戶的家目錄
│ │ a.txt
│ │ b.txt
│ │ c.txt
├─durant # durant用戶的家目錄
│ └─test3
│ └─test4
└─james # james用戶的家目錄
│ a.txt
│ b.txt
│ c.txt
│ test1
│ test2
└─test3
六,程序結構圖

八,程序代碼
1,server
1.1 bin
run.py
# _*_ coding: utf-8 _*_
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
from core import ftp_server
from core import main
from config import settings
if __name__ == '__main__':
a = main.Manager()
a.interactive()
1.2config
settings.py
# _*_ coding: utf-8 _*_ import os import sys import socket import logging BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) ACCOUNTS_FILE = os.path.join(BASE_DIR,'config','accounts.ini') address_family = socket.AF_INET socket_type = socket.SOCK_STREAM BIND_HOST = '127.0.0.1' BIND_PORT = 9999 ip_port = (BIND_HOST,BIND_PORT) coding = 'utf-8' listen_count = 5 max_recv_bytes = 8192 allow_reuser_address = False # 允許配置最大並發數,這里設置為3 MAX_CONCURRENT_COUNT =3
1.3core
auth.py
# _*_ coding: utf-8 _*_
import pickle
import hashlib
import os
import struct
from config import settings
from core.user_handle import UserHandle
from core.file_func import File_func
class Auth_func():
def auth(self, conn):
'''
處理用戶的認證請求
1,根據username讀取accounts.ini文件,然后查看用戶是否存在
2,將程序運行的目錄從bin.user_auth修改到用戶home/username方便之后查詢
3,把客戶端返回用戶的詳細信息
:return:
'''
while True:
user_dic = self.get_recv(conn)
username = user_dic['username']
password = user_dic['password']
md5_obj = hashlib.md5(password.encode('utf-8'))
check_password = md5_obj.hexdigest()
user_handle = UserHandle(username)
# 判斷用戶是否存在 返回列表,
user_data = user_handle.judge_user()
if user_data:
if user_data[0][1] == check_password:
conn.send(struct.pack('i', 1)) # 登錄成功返回 1
self.homedir_path = os.path.join(settings.BASE_DIR, 'home', username)
# 將程序運行的目錄名修改到 用戶home目錄下
os.chdir(self.homedir_path)
# 將用戶配額的大小從M 改到字節
self.quota_bytes = int(user_data[2][1]) * 1024 * 1024
user_info_dic = {
'username': username,
'homedir': user_data[1][1],
'quota': user_data[2][1]
}
# 用戶的詳細信息發送到客戶端
conn.send(pickle.dumps(user_info_dic))
return True
else:
conn.send(struct.pack('i', 0)) # 登錄失敗返回 0
else:
conn.send(struct.pack('i', 0)) # 登錄失敗返回 0
def get_recv(self, conn):
'''從client端接收發來的數據'''
return pickle.loads(conn.recv(settings.max_recv_bytes))
def current_home_size(self):
"""得到當前用戶目錄的大小,字節/M"""
self.home_bytes_size = 0
File_func().recursion_file(self.homedir_path, self.home_bytes_size)
home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)
file_func.py
# _*_ coding: utf-8 _*_
import os
import sys
import struct
import pickle
from config import settings
from core import ftp_server
from core import md5_func
class File_func(object):
def readfile(self,file_path):
'''讀取文件,得到文件內容的bytes類型'''
with open(file_path, 'rb') as f:
filedata = f.read()
return filedata
def send_filedata(self,file_path,conn,exist_file_size=0):
"""下載時,將文件打開,send(data)"""
with open(file_path, 'rb') as f:
f.seek(exist_file_size)
while True:
data = f.read(1024)
if data:
conn.send(data)
else:
break
def write_file(self,conn,f,recv_size,file_size):
'''上傳文件時,將文件內容寫入到文件中'''
while recv_size < file_size:
res = conn.recv(settings.max_recv_bytes)
f.write(res)
recv_size += len(res)
conn.send(struct.pack('i', recv_size)) # 為了進度條的顯示
def recursion_file(self, homedir_path,home_bytes_size):
"""遞歸查詢用戶目錄下的所有文件,算出文件的大小"""
res = os.listdir(homedir_path)
for i in res:
path = os.path.join(homedir_path,i)
if os.path.isdir(path):
self.recursion_file(path,home_bytes_size)
elif os.path.isfile(path):
home_bytes_size += os.path.getsize(path)
def current_home_size(self,homedir_path):
"""得到當前用戶目錄的大小,字節/M"""
self.home_bytes_size =0
self.recursion_file(self.home_bytes_size,homedir_path)
home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)
ftp_server.py
# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import pickle
import subprocess
import hashlib
import queue
from threading import Thread
from threading import currentThread
from config import settings
from core.user_handle import UserHandle
from core.file_func import File_func
from core.md5_func import Md5_func
from core.auth_func import Auth_func
class FTPServer():
def __init__(self,server_address,bind_and_listen = True):
self.server_address = server_address
self.socket = socket.socket(settings.address_family,settings.socket_type)
self.q = queue.Queue(settings.MAX_CONCURRENT_COUNT)
if bind_and_listen:
try:
self.server_bind()
self.server_listen()
except Exception:
self.server_close()
def server_bind(self):
allow_reuse_address = False
if allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
def server_listen(self):
self.socket.listen(settings.listen_count)
def server_close(self):
self.socket.close()
def server_accept(self):
return self.socket.accept()
def conn_close(self, conn):
conn.close()
def server_link(self):
print("\033[31;1mwaiting client .....\033[0m")
while True: # 鏈接循環
conn,self.client_addr = self.server_accept()
print('客戶端地址:', self.client_addr)
# while True: # 通信循環
try:
t = Thread(target=self.server_handle,args=(conn,))
self.q.put(t)
t.start()
except Exception as e:
print(e)
self.conn_close(conn)
self.q.get()
def server_handle(self,conn):
'''處理與用戶的交互指令'''
if Auth_func().auth(conn):
print("\033[32;1m-------user authentication successfully-------\033[0m")
try:
res = conn.recv(settings.max_recv_bytes)
if not res:
self.conn_close(conn)
self.q.get()
# 解析命令,提取相應的參數
self.cmds = res.decode(settings.coding).split()
if hasattr(self, self.cmds[0]):
func = getattr(self, self.cmds[0])
func(conn)
except Exception as e:
print(e)
self.conn_close(conn)
self.q.get()
def get(self, conn):
'''
下載,首先查看文件是否存在,然后上傳文件的報頭大小,上傳文件,以讀的方式發開文件
找到下載的文件
發送 header_size
發送 header_bytes file_size
讀文件 rb 發送 send(line)
若文件不存在,發送0 client提示:文件不存在
:param cmds:
:return:
'''
if len(self.cmds) > 1:
filename = self.cmds[1]
self.file_path = os.path.join(os.getcwd(), filename)
if os.path.isfile(self.file_path):
file_size = os.path.getsize(self.file_path)
obj = conn.recv(4)
exist_file_size = struct.unpack('i', obj)[0]
header = {
'filename': filename,
'filemd5': Md5_func().getfile_md5(self.file_path),
'file_size': file_size
}
header_bytes = pickle.dumps(header)
conn.send(struct.pack('i', len(header_bytes)))
conn.send(header_bytes)
if exist_file_size: # 表示之前被下載過 一部分
if exist_file_size != file_size:
File_func().send_filedata(self.file_path, exist_file_size)
else:
print('\033[31;1mbreakpoint and file size are the same\033[0m')
else: # 文件第一次下載
File_func().send_filedata(self.file_path, conn)
else:
print('\033[31;1merror\033[0m')
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m")
def put(self, conn):
"""從client上傳文件到server當前工作目錄下
"""
if len(self.cmds) > 1:
obj = conn.recv(4)
state_size = struct.unpack('i', obj)[0]
if state_size:
# 算出了home下已被占用的大小self.home_bytes_size
self.current_home_size()
header_bytes = conn.recv(struct.unpack('i', conn.recv(4))[0])
header_dic = pickle.loads(header_bytes)
filename = header_dic.get('filename')
file_size = header_dic.get('file_size')
file_md5 = header_dic.get('file_md5')
self.file_path = os.path.join(os.getcwd(), filename)
if os.path.exists(self.file_path):
conn.send(struct.pack('i', 1))
has_size = os.path.getsize(self.file_path)
if has_size == file_size:
print("\033[31;1mfile already does exist!\033[0m")
conn.send(struct.pack('i', 0))
else:
print('\033[31;1mLast time file not finished,this time continue\033[0m')
conn.send(struct.pack('i', 1))
if self.home_bytes_size + int(file_size - has_size) > self.quota_bytes:
print('\033[31;1mSorry exceeding user quotas\033[0m')
conn.send(struct.pack('i', 0))
else:
conn.send(struct.pack('i', 1))
conn.send(struct.pack('i', has_size))
with open(self.file_path, 'ab') as f:
f.seek(has_size)
File_func().write_file(conn, f, has_size, file_size)
Md5_func().verification_filemd5(self.file_path, conn, file_md5)
else:
conn.send(struct.pack('i', 0))
print('\033[31;1mfile does not exist, now first put\033[0m')
if self.home_bytes_size + int(file_size) > self.quota_bytes:
print('\033[31;1mSorry exceeding user quotas\033[0m')
conn.send(struct.pack('i', 0))
else:
conn.send(struct.pack('i', 1))
with open(self.file_path, 'wb') as f:
recv_size = 0
File_func().write_file(conn, f, recv_size, file_size)
Md5_func().verification_filemd5(self.file_path,conn, file_md5)
else:
print("\033[31;1mfile does not exist!\033[0m")
else:
print("\033[31;1muser does not enter file name\033[0m")
def ls(self, conn):
'''查看當前工作目錄下,先返回文件列表的大小,在返回查詢的結果'''
print("\033[34;1mview current working directory\033[0m")
subpro_obj = subprocess.Popen('dir', shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = subpro_obj.stdout.read()
stderr = subpro_obj.stderr.read()
conn.send(struct.pack('i', len(stdout + stderr)))
conn.send(stdout)
conn.send(stderr)
print('\033[31;1mCongratulations view directory success\033[0m')
def mkdir(self, conn):
'''增加目錄
在當前目錄下,增加目錄
1.查看目錄名是否已經存在
2.增加目錄成功,返回 1
2.增加目錄失敗,返回 0'''
print("\033[34;1madd working directory\033[0m")
if len(self.cmds) > 1:
mkdir_path = os.path.join(os.getcwd(), self.cmds[1])
if not os.path.exists(mkdir_path):
os.mkdir(mkdir_path)
print('\033[31;1mCongratulations add directory success\033[0m')
conn.send(struct.pack('i', 1))
else:
print("\033[31;1muser directory already does exist\033[0m")
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m")
def cd(self, conn):
'''切換目錄
1.查看是否是目錄名
2.拿到當前目錄,拿到目標目錄,
3.判斷homedir是否在目標目錄內,防止用戶越過自己的home目錄 eg: ../../....
4.切換成功,返回 1
5.切換失敗,返回 0'''
print("\033[34;1mSwitch working directory\033[0m")
if len(self.cmds) > 1:
dir_path = os.path.join(os.getcwd(), self.cmds[1])
if os.path.isdir(dir_path):
# os.getcwd 獲取當前工作目錄
previous_path = os.getcwd()
# os.chdir改變當前腳本目錄
os.chdir(dir_path)
target_dir = os.getcwd()
if self.homedir_path in target_dir:
print('\033[31;1mCongratulations switch directory success\033[0m')
conn.send(struct.pack('i', 1))
else:
print('\033[31;1mSorry switch directory failed\033[0m')
# 切換失敗后,返回到之前的目錄下
os.chdir(previous_path)
conn.send(struct.pack('i', 0))
else:
print('\033[31;1mSorry switch directory failed,the directory is not current directory\033[0m')
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m")
def remove(self, conn):
"""刪除指定的文件,或者空文件夾
1.刪除成功,返回 1
2.刪除失敗,返回 0
"""
print("\033[34;1mRemove working directory\033[0m")
if len(self.cmds) > 1:
file_name = self.cmds[1]
file_path = os.path.join(os.getcwd(), file_name)
if os.path.isfile(file_path):
os.remove(file_path)
conn.send(struct.pack('i', 1))
elif os.path.isdir(file_path): # 刪除空目錄
if not len(os.listdir(file_path)):
os.removedirs(file_path)
print('\033[31;1mCongratulations remove success\033[0m')
conn.send(struct.pack('i', 1))
else:
print('\033[31;1mSorry remove directory failed\033[0m')
conn.send(struct.pack('i', 0))
else:
print('\033[31;1mSorry remove directory failed\033[0m')
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m")
main.py
# _*_ coding: utf-8 _*_
from core.user_handle import UserHandle
from core.ftp_server import FTPServer
from config import settings
class Manager():
'''
主程序,包括啟動server,創建用戶,退出
:return:
'''
def __init__(self):
pass
def start_ftp(self):
'''啟動server端'''
server = FTPServer(settings.ip_port)
server.server_link()
server.close()
def create_user(self):
'''創建用戶,執行創建用戶的類'''
username = input("\033[32;1mplease input your username>>>\033[0m").strip()
UserHandle(username).add_user()
def logout(self):
'''
退出登陸
:return:
'''
print("\033[32;1m-------Looking forward to your next login-------\033[0m")
exit()
def interactive(self):
'''交互函數'''
msg = '''\033[32;1m
1 啟動ftp服務端
2 創建用戶
3 退出
\033[0m'''
menu_dic = {
"1": 'start_ftp',
"2": 'create_user',
"3": 'logout',
}
exit_flag = False
while not exit_flag:
print(msg)
user_choice = input("Please input a command>>>").strip()
if user_choice in menu_dic:
getattr(self,menu_dic[user_choice])()
else:
print("\033[31;1myou choice doesn't exist\033[0m")
md5_func.py
# _*_ coding: utf-8 _*_
import hashlib
import struct
from core import ftp_server
from core import file_func
class Md5_func(object):
def getfile_md5(self,file_path):
'''獲取文件的md5'''
md5 = hashlib.md5(file_func.File_func().readfile(file_path))
print("md5是:\n",md5.hexdigest())
return md5.hexdigest()
def handle_data(self):
'''處理接收到的數據,主要是將密碼轉化為md5的形式'''
user_dic = ftp_server.FTPServer().get_recv()
username = user_dic['username']
password = user_dic['password']
md5_obj = hashlib.md5()
md5_obj.update(password)
check_password = md5_obj.hexdigest()
def verification_filemd5(self,file_path,conn,filemd5):
# 判斷文件內容的md5
if self.getfile_md5(file_path) == filemd5:
print('\033[31;1mCongratulations download success\033[0m')
conn.send(struct.pack('i', 1))
else:
print('\033[31;1mSorry download failed\033[0m')
conn.send(struct.pack('i', 0))
user_handle.py
#_*_coding:utf-8_*_
import configparser
import hashlib
import os
from config import settings
class UserHandle():
'''
創建用戶名稱,密碼
如果用戶存在,則返回,如果用戶不存在,則注冊成功
'''
def __init__(self,username):
self.username = username
self.config = configparser.ConfigParser()
self.config.read(settings.ACCOUNTS_FILE)
def password(self):
'''生成用戶的密碼,然后加密'''
password_inp = input("\033[32;1mplease input your password>>>\033[0m").strip()
md5_obj = hashlib.md5()
md5_obj.update(password_inp.encode('utf-8'))
md5_password = md5_obj.hexdigest()
return md5_password
def disk_quota(self):
'''生成每個用戶的磁盤配額'''
quota = input('\033[32;1mplease input Disk quotas>>>:\033[0m').strip()
if quota.isdigit():
return quota
else:
exit('\033[31;1mdisk quotas must be integer\033[0m')
def add_user(self):
'''創建用戶,存到accounts.ini'''
if not self.config.has_section(self.username):
print('\033[31;1mcreating username is :%s \033[0m' % self.username)
self.config.add_section(self.username)
self.config.set(self.username,'password',self.password())
self.config.set(self.username, 'homedir', 'home/' + self.username)
self.config.set(self.username, 'quota', self.disk_quota())
self.write_config()
self.create_userhome()
print('\033[1;32msuccessfully create userdata\033[0m')
else:
print('\033[1;31musername already existing\033[0m')
def create_userhome(self):
'''創建用戶的home文件夾'''
os.mkdir(os.path.join(settings.BASE_DIR, 'home', self.username))
def write_config(self):
'''寫入文檔'''
with open(settings.ACCOUNTS_FILE,'w') as f:
self.config.write(f)
def judge_user(self):
'''判斷用戶是否存在'''
if self.config.has_section(self.username):
return self.config.items(self.username)
2,client
2.1bin
run.py
# _*_ coding: utf-8 _*_
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
from core import ftp_client
from config import settings
if __name__ == '__main__':
run = ftp_client.FTPClient(settings.ip_port)
run.execute()
2.2config
settings.py
# _*_ coding: utf-8 _*_ import os import sys import socket BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) # 下載的文件存放路徑 down_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'download') # 上傳的文件存放路徑 upload_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'upload') #綁定的IP地址 BIND_HOST = '127.0.0.1' #綁定的端口號 BIND_PORT = 9999 ip_port = (BIND_HOST,BIND_PORT) address_family = socket.AF_INET socket_type = socket.SOCK_STREAM coding = 'utf-8' listen_count = 5 max_recv_bytes = 8192 allow_reuser_address = False
2.3core
file_func.py
# _*_ coding: utf-8 _*_
import os
import sys
import struct
import pickle
from config import settings
from core import ftp_client
from core import md5_func
from core import progress_bar_func
class File_func(object):
def readfile(self,file_path):
'''讀取文件'''
with open(file_path,'rb') as f:
filedata = f.read()
return filedata
def appendfile_content(self,socket,file_path,temp_file_size,file_size):
'''追加文件內容'''
with open(file_path,'ab') as f:
f.seek(temp_file_size)
get_size = temp_file_size
while get_size < file_size:
res = socket.recv(settings.max_recv_bytes)
f.write(res)
get_size += len(res)
progress_bar_func.progress_bar(1,get_size,file_size) #1表示下載
def write_file(self,socket,f,get_size,file_size):
'''下載文件,將內容寫入文件中'''
while get_size < file_size:
res = socket.recv(settings.max_recv_bytes)
f.write(res)
get_size += len(res)
progress_bar_func.progress_bar(1,get_size,file_size) #1表示下載
def recv_file_header(self,socket,header_size):
"""接收文件的header, filename file_size file_md5"""
header_types = socket.recv(header_size)
header_dic = pickle.loads(header_types)
print(header_dic, type(header_dic))
total_size = header_dic['file_size']
filename = header_dic['filename']
filemd5 = header_dic['filemd5']
return (filename,total_size,filemd5)
def open_sendfile(self,file_size,file_path,socket,recv_size =0):
'''打開要上傳的文件(由於本程序上傳文件的原理是先讀取本地文件,再寫到上傳地址的文件)'''
with open(file_path, 'rb') as f:
f.seek(recv_size)
while True:
data = f.read(1024)
if data:
socket.send(data)
obj = socket.recv(4)
recv_size = struct.unpack('i', obj)[0]
progress_bar_func.progress_bar(2, recv_size, file_size)
else:
break
success_state = struct.unpack('i', socket.recv(4))[0]
if success_state:
print('\033[31;1mCongratulations upload success\033[0m')
else:
print('\033[31;1mSorry upload directory failed\033[0m')
ftp_client.py
# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import sys
import pickle
import hashlib
from config import settings
from core.md5_func import Md5_func
from core.file_func import File_func
from core import progress_bar_func
class FTPClient:
def __init__(self,server_address,connect = True):
self.server_address = server_address
self.socket = socket.socket(settings.address_family,settings.socket_type)
if connect:
try:
self.client_connect()
except Exception:
self.client_close()
def client_connect(self):
try:
self.socket.connect(self.server_address)
except Exception as e:
print("\033[31;1merror:%s\033[0m"%e)
exit("\033[31;1m\nThe server is not activated \033[0m")
def client_close(self):
self.socket.close()
def get(self,cmds):
"""從server下載文件到client
"""
if len(cmds) >1:
filename = cmds[1]
self.file_path = os.path.join(settings.down_filepath, filename)
if os.path.isfile(self.file_path): #如果文件存在,支持斷電續傳
temp_file_size = os.path.getsize(self.file_path)
self.socket.send(struct.pack('i',temp_file_size))
header_size = struct.unpack('i',self.socket.recv(4))[0]
if header_size:
filename,file_size,filemd5 = File_func().recv_file_header(self.socket,header_size)
if temp_file_size == file_size:
print('\033[34;1mFile already does exist\033[0m')
else:
print('\033[34;1mFile now is breakpoint continuation\033[0m')
File_func().appendfile_content(self.socket,self.file_path,temp_file_size,file_size)
Md5_func().verification_filemd5(self.file_path,filemd5)
else:
print("\033[34;1mFile was downloaded before,but now server's file is not exist\033[0m")
else:#如果文件不存在,則是直接下載
self.socket.send(struct.pack('i',0))
obj = self.socket.recv(1024)
header_size = struct.unpack('i', obj)[0]
if header_size==0:
print("\033[31;1mfile does not exist!\033[0m")
else:
filename, file_size, filemd5 = File_func().recv_file_header(self.socket,header_size)
download_filepath = os.path.join(settings.down_filepath, filename)
with open(download_filepath, 'wb') as f:
get_size = 0
File_func().write_file(self.socket,f, get_size, file_size)
Md5_func().verification_filemd5(self.file_path,filemd5)
else:
print("\033[31;1muser does not enter file name\033[0m")
def ls(self,cmds):
'''查看當前工作目錄,文件列表'''
print("\033[34;1mview current working directory\033[0m")
obj = self.socket.recv(4)
dir_size = struct.unpack('i',obj)[0]
recv_size = 0
recv_bytes = b''
while recv_size <dir_size:
temp_bytes = self.socket.recv(settings.max_recv_bytes)
recv_bytes +=temp_bytes
recv_size += len(temp_bytes)
print(recv_bytes.decode('gbk'))
print('\033[31;1mCongratulations view directory success\033[0m')
def mkdir(self,cmds):
'''增加目錄
1,server返回1 增加成功
2,server返回2 增加失敗'''
print("\033[34;1madd working directory\033[0m")
obj = self.socket.recv(4)
res = struct.unpack('i',obj)[0]
if res:
print('\033[31;1mCongratulations add directory success\033[0m')
else:
print('\033[31;1mSorry add directory failed\033[0m')
def cd(self,cmds):
'''切換目錄'''
print("\033[34;1mSwitch working directory\033[0m")
if len(cmds) >1:
obj = self.socket.recv(4)
res = struct.unpack('i', obj)[0]
if res:
print('\033[31;1mCongratulations switch directory success\033[0m')
else:
print('\033[31;1mSorry switch directory failed\033[0m')
else:
print("\033[31;1muser does not enter file name\033[0m")
def remove(self,cmds):
'''表示刪除文件或空文件夾'''
print("\033[34;1mRemove working directory\033[0m")
obj = self.socket.recv(4)
res = struct.unpack('i', obj)[0]
if res:
print('\033[31;1mCongratulations remove success\033[0m')
else:
print('\033[31;1mSorry remove directory failed\033[0m')
def put_situation(self,file_size,condition=0):
'''上傳的時候有兩種情況,文件已經存在,文件不存在'''
quota_state= struct.unpack('i', self.socket.recv(4))[0]
if quota_state:
if condition:
obj = self.socket.recv(4)
recv_size = struct.unpack('i', obj)[0]
File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0)
else:
File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0)
else:
print('\033[31;1mSorry exceeding user quotas\033[0m')
def put(self,cmds):
"""往server端登錄的用戶目錄下上傳文件
"""
if len(cmds) > 1:
filename = cmds[1]
file_path = os.path.join(settings.upload_filepath, filename)
if os.path.isfile(file_path): # 如果文件存在,支持斷電續傳
self.socket.send(struct.pack('i', 1))
self.file_path = file_path
file_size = os.path.getsize(self.file_path)
header_dic = {
'filename': os.path.basename(filename),
'file_md5': Md5_func().getfile_md5(self.file_path),
'file_size': file_size
}
header_bytes = pickle.dumps(header_dic)
self.socket.send(struct.pack('i', len(header_bytes)))
self.socket.send(header_bytes)
state = struct.unpack('i', self.socket.recv(4))[0]
if state: #已經存在
has_state = struct.unpack('i', self.socket.recv(4))[0]
if has_state:
self.put_situation(file_size, 1)
else: # 存在的大小 和文件大小一致 不必再傳
print("\033[31;1mfile already does exist!\033[0m")
else: # 第一次傳
self.put_situation(file_size)
else: # 文件不存在
print("\033[31;1mfile does not exist!\033[0m")
self.socket.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m")
def get_recv(self):
'''從client端接受發來的數據'''
return pickle.loads(self.socket.recv(settings.max_recv_bytes))
def login(self):
'''
登陸函數,當登陸失敗超過三次,則退出
用戶密碼發送到server短
接受server端返回的信息,如果成功返回1,失敗返回0
:return: 如果用戶賬號密碼正確,則返回用戶數據的字典
'''
retry_count = 0
while retry_count <3:
username = input('\033[34;1mplease input Username:\033[0m').strip()
if not username:
continue
password = input('\033[34;1mplease input Password:\033[0m').strip()
user_dic = {
'username':username,
'password':password
}
#將用戶信息發送到客戶端,然后接受客戶端的數據
data = pickle.dumps(user_dic)
self.socket.send(pickle.dumps(user_dic))
#為了防止出現黏包問題,所以先解壓報頭,讀取報頭,再讀數據
obj = self.socket.recv(4)
res = struct.unpack('i',obj)[0]
#此處,如果返回的是代碼4001,則成功 4002則失敗
if res:
print("\033[32;1m-----------------welcome to ftp client-------------------\033[0m")
user_info_dic = self.get_recv()
recv_username = user_info_dic['username']
return True
else:
print("\033[31;1mAccount or Passwordoes not correct!\033[0m")
retry_count +=1
def execute(self):
'''
執行,或者實施
:return:
'''
if self.login():
while True:
try:
self.help_info()
inp = input("Please input a command>>>").strip()
if not inp:
continue
self.socket.send(inp.encode(settings.coding))
cmds = inp.split()
if hasattr(self, cmds[0]):
func = getattr(self, cmds[0])
func(cmds)
break
else:
print('\033[31;1mNo such command ,please try again\033[0m')
except Exception as e: # server關閉了
print('\033[31;1m%s\033[0m'%e)
break
def help_info(self):
print ('''\033[34;1m
get + (文件名) 表示下載文件
put + (文件名) 表示上傳文件
ls 表示查詢當前目錄下的文件列表(只能訪問自己的文件列表)
mkdir + (文件名) 表示創建文件夾
cd + (文件名) 表示切換目錄(只能在自己的文件列表中切換)
remove + (文件名) 表示刪除文件或空文件夾
\033[0m''')
md5_func.py
# _*_ coding: utf-8 _*_
import hashlib
from core import ftp_client
from core.file_func import File_func
class Md5_func(object):
def getfile_md5(self,file_path):
'''對文件內容進行加密,也就是保持文件的一致性'''
md5 = hashlib.md5(File_func().readfile(file_path))
print("md5是:\n",md5.hexdigest())
return md5.hexdigest()
def verification_filemd5(self,file_path,filemd5):
# 判斷下載下來的文件MD5值和server傳過來的MD5值是否一致
if self.getfile_md5(file_path) == filemd5:
print('\033[31;1mCongratulations download success\033[0m')
else:
print('\033[31;1mSorry download failed,download again support breakpoint continuation\033[0m')
progress_bar_fun.py
# _*_ coding: utf-8 _*_
'''進度條的表示功能'''
import sys
def progress_bar(num, get_size, file_size):
float_rate = float(get_size) / float(file_size)
rate_num = round(float_rate * 100, 2)
if num == 1: # 1表示下載
sys.stdout.write('\033[31;1m\rfinish downloaded perentage:{0}%\033[0m'.format(rate_num))
elif num == 2: # 2表示上傳
sys.stdout.write('\033[31;1m\rfinish uploaded perentage:{0}%\033[0m'.format(rate_num))
sys.stdout.flush()
