FTP

1、程序介紹:

需求:

支持多用戶在線的FTP程序

要求:
一、用戶加密認證
二、容許同時多用戶登陸
三、每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄
四、對用戶進行磁盤配額,每一個用戶的可用空間不一樣
五、容許用戶在ftp server上隨意切換目錄
六、容許用戶查看當前目錄下文件
七、容許上傳和下載文件,保證文件一致性
八、文件傳輸過程當中顯示進度條
九、附加功能:支持文件的斷點續傳

實現功能:
用戶加密認證
容許同時多用戶登陸
每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄
容許上傳和下載文件,保證文件一致性
文件傳輸過程當中顯示進度條

程序結構:

FTP服務端
FtpServer  #服務端主目錄
├── bin  #啓動目錄
│   └── ftp_server.py #啓動文件
├── conf  #配置文件目錄
│   ├── accounts.cfg #用戶存儲
│   └── settings.py #配置文件
├── core  #程序主邏輯目錄
│   ├── ftp_server.py #功能文件
│   └── main.py #主邏輯文件
├── home  #用戶家目錄
│   ├── test001  #用戶目錄
│   └── test002  #用戶目錄
└── log  #日誌目錄

FTP客戶端
FtpClient  #客戶端主目錄
└── ftp_client.py #客戶端執行文件


目錄

2、流程圖






3、代碼

FtpServer

bin/ftp_server.py
複製代碼
 1 #!/usr/bin/env python
 2 #_*_coding:utf-8_*_
 3 
 4 import os
 5 import sys
 6 
 7 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 8 sys.path.append(BASE_DIR)
 9 
10 from core import main
11 
12 if __name__ == '__main__':
13     main.ArvgHandler()
複製代碼
 
conf/accounts.cfg
複製代碼
1 [DEFAULT]
2 
3 [test001]
4 Password = 123
5 Quotation = 100
6 
7 [test002]
8 Password = 123
9 Quotation = 100
複製代碼
 
conf/settings.py
複製代碼
 1 #!/usr/bin/env python
 2 #_*_coding:utf-8_*_
 3 
 4 import os
 5 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 6 
 7 USER_HOME = "%s/home" % BASE_DIR
 8 LOG_DIR = "%s/log" % BASE_DIR
 9 LOG_LEVEL = "DEBUG"
10 
11 ACCOUNT_FILE = "%s/conf/accounts.cfg" % BASE_DIR
12 
13 HOST = "127.0.0.1"
14 PORT = 9999
複製代碼
 
core/ftp_server.py
複製代碼
  1 #!/usr/bin/env python
  2 #_*_coding:utf-8_*_
  3 
  4 import socketserver
  5 import json
  6 import configparser
  7 import os
  8 import hashlib
  9 from conf import settings
 10 
 11 STATUS_CODE = {
 12     250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}",
 13     251:"Invalid cmd",
 14     252:"Invalid auth data",
 15     253:"Wrong username or password",
 16     254:"Passed authentication",
 17     255:"filename doesn't provided",
 18     256:"File doesn't exist on server",
 19     257:"ready to send file",
 20     258:"md5 verification",
 21 }
 22 
 23 '''
 24 250:「無效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}」,
 25 251:「無效的CMD」,
 26 252:「驗證數據無效」,
 27 253:「錯誤的用戶名或密碼」,
 28 254:「經過身份驗證」,
 29 255:「文件名不提供」,
 30 256:「服務器上不存在文件」,
 31 257:「準備發送文件」,
 32 258:「md5驗證」,
 33 '''
 34 
 35 class FTPHandler(socketserver.BaseRequestHandler):
 36 
 37     def handle(self):
 38         '''接收客戶端消息(用戶,密碼,action)'''
 39         while True:
 40             self.data = self.request.recv(1024).strip()
 41             print(self.client_address[0])
 42             print(self.data)
 43             # self.request.sendall(self.data.upper())
 44 
 45             if not self.data:
 46                 print("client closed...")
 47                 break
 48             data = json.loads(self.data.decode())  #接收客戶端消息
 49             if data.get('action') is not None:  #action不爲空
 50                 print("---->", hasattr(self, "_auth"))
 51                 if hasattr(self, "_%s" % data.get('action')): #客戶端action 符合服務端action
 52                     func = getattr(self, "_%s" % data.get('action'))
 53                     func(data)
 54                 else:  #客戶端action 不符合服務端action
 55                     print("invalid cmd")
 56                     self.send_response(251)  # 251:「無效的CMD」
 57             else:  #客戶端action 不正確
 58                 print("invalid cmd format")
 59                 self.send_response(250) # 250:「無效的cmd格式,例如:{'action':'get','filename':'test.py','size':344}」
 60 
 61     def send_response(self,status_code,data=None):
 62         '''向客戶端返回數據'''
 63         response = {'status_code':status_code,'status_msg':STATUS_CODE[status_code]}
 64         if data:
 65             response.update(data)
 66         self.request.send(json.dumps(response).encode())
 67 
 68     def _auth(self,*args,**kwargs):
 69         '''覈對服務端 發來的用戶,密碼'''
 70         # print("---auth",args,kwargs)
 71         data = args[0]
 72         if data.get("username") is None or data.get("password") is None: #客戶端的用戶和密碼有一個爲空 則返回錯誤
 73             self.send_response(252)  # 252:「驗證數據無效」
 74 
 75         user = self.authenticate(data.get("username"),data.get("password")) #把客戶端的用戶密碼進行驗證合法性
 76         if user is None: #客戶端的數據爲空 則返回錯誤
 77             self.send_response(253)  # 253:「錯誤的用戶名或密碼」
 78         else:
 79             print("password authentication",user)
 80             self.user = user
 81             self.send_response(254)  # 254:「經過身份驗證」
 82 
 83     def authenticate(self,username,password):
 84         '''驗證用戶合法性,合法就返回數據,覈對本地數據'''
 85         config = configparser.ConfigParser()
 86         config.read(settings.ACCOUNT_FILE)
 87         if username in config.sections():  #用戶匹配成功
 88             _password = config[username]["Password"]
 89             if _password == password:  #密碼匹配成功
 90                 print("pass auth..",username)
 91                 config[username]["Username"] = username
 92                 return config[username]
 93 
 94     def _put(self,*args,**kwargs):
 95         "client send file to server"
 96         data = args[0]
 97         base_filename = data.get('filename')
 98         file_obj = open(base_filename, 'wb')
 99         data = self.request.recv(4096)
100         file_obj.write(data)
101         file_obj.close()
102 
103     def _get(self,*args,**kwargs):
104         '''get 下載方法'''
105         data = args[0]
106         if data.get('filename') is None:
107             self.send_response(255)  # 255:「文件名不提供」,
108         user_home_dir = "%s/%s" %(settings.USER_HOME,self.user["Username"]) #當前鏈接用戶的目錄
109         file_abs_path = "%s/%s" %(user_home_dir,data.get('filename'))  #客戶端發送過來的目錄文件
110         print("file abs path",file_abs_path)
111 
112         if os.path.isfile(file_abs_path):  #客戶端目錄文件名 存在服務端
113             file_obj = open(file_abs_path,'rb')  # 用bytes模式打開文件
114             file_size = os.path.getsize(file_abs_path)  #傳輸文件的大小
115             self.send_response(257,data={'file_size':file_size}) #返回即將傳輸的文件大小 和狀態碼
116 
117             self.request.recv(1)  #等待客戶端確認
118 
119             if data.get('md5'): #有 --md5 則傳輸時加上加密
120                 md5_obj = hashlib.md5()
121                 for line in file_obj:
122                     self.request.send(line)
123                     md5_obj.update(line)
124                 else:
125                     file_obj.close()
126                     md5_val = md5_obj.hexdigest()
127                     self.send_response(258,{'md5':md5_val})
128                     print("send file done....")
129             else:  #沒有 --md5  直接傳輸文件
130                 for line in file_obj:
131                     self.request.send(line)
132                 else:
133                     file_obj.close()
134                     print("send file done....")
135 
136         else:
137             self.send_response(256) # 256:「服務器上不存在文件」=
138 
139 
140     def _ls(self,*args,**kwargs):
141         pass
142 
143     def _cd(self,*args,**kwargs):
144         pass
145     
146 
147 if __name__ == '__main__':
148     HOST, PORT = "127.0.0.1", 9999
複製代碼
 
core/main.py
複製代碼
 1 #!/usr/bin/env python
 2 #_*_coding:utf-8_*_
 3 
 4 import optparse
 5 from core.ftp_server import FTPHandler
 6 import socketserver
 7 from conf import settings
 8 
 9 class ArvgHandler(object):
10     def __init__(self):
11         self.parser = optparse.OptionParser()
12         # parser.add_option("-s","--host",dest="host",help="server binding host address")
13         # parser.add_option("-p","--port",dest="port",help="server binding port")
14         (options, args) = self.parser.parse_args()
15         # print("parser",options,args)
16         # print(options.host,options.port)
17         self.verify_args(options, args)
18 
19     def verify_args(self,options,args):
20         '''校驗並調用相應功能'''
21         if hasattr(self,args[0]):
22             func = getattr(self,args[0])
23             func()
24         else:
25             self.parser.print_help()
26 
27     def start(self):
28         print('---going to start server---')
29 
30         server = socketserver.ThreadingTCPServer((settings.HOST, settings.PORT), FTPHandler)
31 
32         server.serve_forever()
複製代碼
 
FtpClient
ftp_client.py
複製代碼
  1 #!/usr/bin/env python
  2 #_*_coding:utf-8_*_
  3 
  4 import socket
  5 import os
  6 import sys
  7 import optparse
  8 import json
  9 import hashlib
 10 
 11 STATUS_CODE = {
 12     250:"Invalid cmd format, e.g:{'action':'get','filename':'test.py','size':344}",
 13     251:"Invalid cmd",
 14     252:"Invalid auth data",
 15     253:"Wrong username or password",
 16     254:"Passed authentication",
 17     255:"filename doesn't provided",
 18     256:"File doesn't exist on server",
 19     257:"ready to send file",
 20 }
 21 
 22 class FTPClient(object):
 23     def __init__(self):
 24         parser = optparse.OptionParser()
 25         parser.add_option("-s","--server",dest="server",help="ftp server ip_addr")
 26         parser.add_option("-P","--port",type="int",dest="port",help="ftp server port")
 27         parser.add_option("-u","--username",dest="username",help="username")
 28         parser.add_option("-p","--password",dest="password",help="password")
 29 
 30         self.options,self.args = parser.parse_args()
 31         self.verify_args(self.options,self.args)
 32         self.make_connection()
 33 
 34     def make_connection(self):
 35         '''遠程鏈接'''
 36         self.sock = socket.socket()
 37         self.sock.connect((self.options.server,self.options.port))
 38 
 39     def verify_args(self,options,args):
 40         '''校驗參數合法性'''
 41         if options.username is not None and options.password is not None:  #用戶和密碼,兩個都不爲空
 42             pass
 43         elif options.username is None and options.password is None: #用戶和密碼,兩個都爲空
 44             pass
 45         else:  #用戶和密碼,有一個爲空
 46             # options.username is None or options.password is None:  #用戶和密碼,有一個爲空
 47             exit("Err: username and password must be provided together...")
 48 
 49         if options.server and options.port:
 50             # print(options)
 51             if options.port >0 and options.port <65535:
 52                 return True
 53             else:
 54                 exit("Err:host port must in 0-65535")
 55 
 56     def authenticate(self):
 57         '''用戶驗證,獲取客戶端輸入信息'''
 58         if self.options.username:  #有輸入信息 發到遠程判斷
 59             print(self.options.username,self.options.password)
 60             return self.get_auth_result(self.options.username,self.options.password)
 61         else:  #沒有輸入信息 進入交互式接收信息
 62             retry_count = 0
 63             while retry_count <3:
 64                 username = input("username: ").strip()
 65                 password = input("password: ").strip()
 66                 return self.get_auth_result(username,password)
 67                 # retry_count +=1
 68 
 69     def get_auth_result(self,user,password):
 70         '''遠程服務器判斷 用戶,密碼,action '''
 71         data = {'action':'auth',
 72                 'username':user,
 73                 'password':password,}
 74 
 75         self.sock.send(json.dumps(data).encode())  #發送 用戶,密碼,action 到遠程服務器  等待遠程服務器的返回結果
 76         response = self.get_response()  #獲取服務器返回碼
 77         if response.get('status_code') == 254: #經過驗證的服務器返回碼
 78             print("Passed authentication!")
 79             self.user = user
 80             return True
 81         else:
 82             print(response.get("status_msg"))
 83 
 84     def get_response(self):
 85         '''獲得服務器端回覆結果,公共方法'''
 86         data = self.sock.recv(1024)
 87         data = json.loads(data.decode())
 88         return data
 89 
 90     def interactive(self):
 91         '''交互程序'''
 92         if self.authenticate(): #認證成功,開始交互
 93             print("--start interactive iwth u...")
 94             while True: #循環 輸入命令方法
 95                 choice = input("[%s]:"%self.user).strip()
 96                 if len(choice) == 0:continue
 97                 cmd_list = choice.split()
 98                 if hasattr(self,"_%s"%cmd_list[0]): #反射判斷 方法名存在
 99                     func = getattr(self,"_%s"%cmd_list[0]) #反射 方法名
100                     func(cmd_list)  #執行方法
101                 else:
102                     print("Invalid cmd.")
103 
104     def _md5_required(self,cmd_list):
105         '''檢測命令是否須要進行MD5的驗證'''
106         if '--md5' in cmd_list:
107             return True
108 
109     def show_progress(self,total):
110         '''進度條'''
111         received_size = 0
112         current_percent = 0
113         while received_size < total:
114             if int((received_size / total) * 100) > current_percent :
115                 print("#",end="",flush=True)
116                 current_percent = (received_size / total) * 100
117             new_size = yield
118             received_size += new_size
119 
120     def _get(self,cmd_list):
121         ''' get 下載方法'''
122         print("get--",cmd_list)
123         if len(cmd_list) == 1:
124             print("no filename follows...")
125             return
126         #客戶端操做信息
127         data_header = {
128             'action':'get',
129             'filename':cmd_list[1],
130         }
131 
132         if self._md5_required(cmd_list):  #命令請求裏面有帶 --md5
133             data_header['md5'] = True  #將md5加入 客戶端操做信息
134 
135         self.sock.send(json.dumps(data_header).encode()) #發送客戶端的操做信息
136         response = self.get_response()  #接收服務端返回的 操做信息
137         print(response)
138 
139         if response["status_code"] ==257: #服務端返回的狀態碼是:傳輸中
140             self.sock.send(b'1')  # send confirmation to server
141             base_filename = cmd_list[1].split('/')[-1] #取出要接收的文件名
142             received_size = 0  #本地接收總量
143             file_obj = open(base_filename,'wb') #bytes模式寫入
144 
145             if self._md5_required(cmd_list): #命令請求裏有 --md5
146                 md5_obj = hashlib.md5()
147 
148                 progress = self.show_progress(response['file_size'])
149                 progress.__next__()
150 
151                 while received_size < response['file_size']: #當接收的量 小於 文件總量 就循環接收文件
152                     data = self.sock.recv(4096) #一次接收4096
153                     received_size += len(data) #本地接收總量每次遞增
154 
155                     try:
156                         progress.send(len(data))
157                     except StopIteration as e:
158                         print("100%")
159 
160                     file_obj.write(data) #把接收的數據 寫入文件
161                     md5_obj.update(data) #把接收的數據 md5加密
162                 else:
163                     print("--->file rece done<---") #成功接收文件
164                     file_obj.close() #關閉文件句柄
165                     md5_val = md5_obj.hexdigest()
166                     md5_from_server = self.get_response()  #獲取服務端發送的 md5
167                     if md5_from_server['status_code'] ==258:  #狀態碼爲258
168                         if md5_from_server['md5'] == md5_val:  #兩端 md5值 對比
169                             print("%s 文件一致性校驗成功!" %base_filename)
170                     # print(md5_val,md5_from_server)
171             else:  #沒有md5校驗 直接收文件
172                 progress = self.show_progress(response['file_size'])
173                 progress.__next__()
174 
175                 while received_size < response['file_size']: #當接收的量 小於 文件總量 就循環接收文件
176                     data = self.sock.recv(4096) #一次接收4096
177                     received_size += len(data) #本地接收總量每次遞增
178                     file_obj.write(data) #把接收的數據 寫入文件
179                     try:
180                         progress.send(len(data))
181                     except StopIteration as e:
182                         print("100%")
183                 else:
184                     print("--->file rece done<---") #成功接收文件
185                     file_obj.close() #關閉文件句柄
186 
187     def _put(self,cmd_list):
188         ''' put 下載方法'''
189         print("put--", cmd_list)
190         if len(cmd_list) == 1:
191             print("no filename follows...")
192             return
193         # 客戶端操做信息
194         data_header = {
195             'action': 'put',
196             'filename': cmd_list[1],
197         }
198         self.sock.send(json.dumps(data_header).encode())  # 發送客戶端的操做信息
199         self.sock.recv(1)
200         file_obj = open(cmd_list[1],'br')
201         for line in file_obj:
202             self.sock.send(line)
203 
204 
205 if __name__ == '__main__':
206     ftp = FTPClient()
207     ftp.interactive()
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息