python【项目】:基于socket的FTP服务器

  • 功能要求

1. 用户加密认证

2. 服务端采用 SocketServer实现,支持多客户端连接

3. 每个用户有自己的家目录且只能访问自己的家目录

4. 对用户进行磁盘配额、不同用户配额可不同

5. 用户可以登陆server后,可切换目录

6. 能查看当前目录下文件

7. 上传下载文件,保证文件一致性

8. 传输过程中实现进度条展示

9.用户可在自己家目录进行创建目录、文件、删除目录及文件

10.服务端可实现增加用户、删除用户

11.支持上传时断点续传

  • 应用知识点 

a) 类的应用

b) 函数的使用

c) 多进程

d) 反射

e)  socket、socketserver、hashlib、configparser、logging

f)  文件的读写

  • 开发环境
  1. python 3.6.1
  2. PyCharm 2016.2.3
  • 目录结构

FTPClient

       |--bin              (主接口目录)

      |--ftpclient.py    (客户端主程序接口文件)

      |--config           (配置文件目录)

      |--code.py       (状态码文件)

      |--settings.py    (配置文件)

      |--template.py   (模板文件)

      |--download        (下载存放目录)

      |--lib              (模块目录)

      |--client.py      (客户端各类接口封装)

      |--common.py   (公共接口)

      |--logs            (日志目录)

      |--ftpclient.log  (日志文件)

  |--clientRun.py     (主执行程序)

 

FTPServer

       |--bin               (主接口目录)

      |--ftpserver.py      (服务端socket接口文件)

        |--main.py        (主程序接口文件)

      |--config            (配置目录)

      |--settings.py     (配置文件)

      |--template.py    (模板文件)

  |--database          (数据保存目录)

   |--user.ini        (用户信息文件)

  |--dbhelper          (数据目录)

     |--dbapi.py       (数据操作接口)

  |--lib               (模块目录)

     |--user.py        (用户类文件用来实例化对象)

      |--server.py      (服务端模块,各类所有命令方法)

      |--common.py    (公共模块文件)

      |--logs

      |--ftpserver.log   (日志文件)

   |--upload           (上传文件存放的目录)

   |--serverRun.py     (主执行程序)

  • 模块功能系统图

1、思维导图

2、功能接口关系图

客户端:

服务端:

  • 关键代码段

1、服务端

 1 #!/usr/bin/env python
 2 #coding=utf-8
 3 __author__ = 'yinjia'
 4 
 5 
 6 import socketserver,os,sys
 7 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 8 from config import settings,template
 9 from lib import common,server
10 
11 
12 
13 logger = common.Logger('ftpserver').getlog()
14 
15 class MyServer(socketserver.BaseRequestHandler):
16 
17     def handle(self):
18         try:
19             client_socket = self.request
20             client_addr = self.client_address
21             logger.info("client {0} connected".format(client_addr))
22             #发送成功标识给客户端
23             client_socket.sendall(bytes("OK",encoding='utf-8'))
24             client_user = None
25 
26             while True:
27                 #获取客户端命令
28                 ret_client_data = str(client_socket.recv(1024),encoding='utf-8')
29 
30                 #判断客户端是否退出
31                 if ret_client_data == b'':
32                     logger.info("client {0} is exit".format(client_addr))
33                     client_socket.close()
34 
35                 #取出客户端命令
36                 cmd = ret_client_data.split("|")[0]
37 
38                 logger.info("client {0} send command {1}".format(client_addr,cmd))
39                 #判断是否登录认证状态
40                 if cmd == 'auth':
41                     client_user = server.client_auth(client_socket, ret_client_data)
42                 else:
43                    try:
44                         #通过反射寻找模块的命令
45                         if hasattr(server,cmd):
46                             func = getattr(server,cmd)
47                             func(client_socket, client_user, ret_client_data)
48                         else:
49                             logger.error("command {0} not found".format(cmd))
50                    except Exception as e:
51                        logger.error(e)
52                        client_socket.close()
53 
54         except Exception as e:
55             logger.error(e)
56 
57 def process():
58     """
59     启动服务
60     :return:
61     """
62     server = socketserver.ThreadingTCPServer((settings.FTP_SERVER_IP,settings.FTP_SERVER_PORT),MyServer)
63     server.serve_forever()
ftpserver
 1 #!/usr/bin/env python
 2 #coding=utf-8
 3 __author__ = 'yinjia'
 4 
 5 
 6 import os,sys,configparser
 7 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 8 from config import settings
 9 
10 def readall_sections():
11     """
12     读取user.ini文件所有的用户名
13     :return: 返回所有的用户名列表
14     """
15     con = configparser.ConfigParser()
16     con.read(settings.USER_INI, encoding='utf-8')
17     result = con.sections()
18     return result
19 
20 def GetValue(key,value):
21     """
22     获取user.ini文件键名值
23     :param key: 键名
24     :param value: 键值
25     :return:
26     """
27     con = configparser.ConfigParser()
28     con.read(settings.USER_INI, encoding='utf-8')
29     result = con.get(key,value)
30     return result
31 
32 def CheckSections(sections_name):
33     """
34     检查sections项名是否存在
35     :param sections_name: 用户名
36     :return:
37     """
38     con = configparser.ConfigParser()
39     con.read(settings.USER_INI, encoding='utf-8')
40     result = con.has_section(sections_name)
41     return result
42 
43 def AddOption(sections_name, **args):
44     """
45     添加用户信息
46     :param sections_name:用户名
47     :param args: 字典格式:('test3',password='aa',totalspace='bb',userspace='cc')
48     :return:
49     """
50     con = configparser.ConfigParser()
51     with open(settings.USER_INI,'a+',encoding='utf-8') as f:
52         con.add_section(sections_name)
53         for key in args:
54             con.set(sections_name, key, args[key])
55         con.write(f)
56 
57 def DelSections(sections_name):
58     """
59     删除用户信息
60     :param sections_name:
61     :return:
62     """
63     con = configparser.ConfigParser()
64     con.read(settings.USER_INI, encoding='utf-8')
65     with open(settings.USER_INI,'w') as f:
66         con.remove_section(sections_name)
67         con.write(f)
68 
69 def ModifyOption(sections_name, **args):
70     """
71     修改磁盘配额空间
72     :param sections_name: 用户名
73     :param args:用户字典信息
74     :return:
75     """
76     con = configparser.ConfigParser()
77     con.read(settings.USER_INI, encoding='utf-8')
78     for key in args:
79         con.set(sections_name, key, args[key])
80     with open(settings.USER_INI, 'w', encoding='utf-8') as f:
81         con.write(f)
82 
83 def load_info(sections_name):
84     """
85     加载用户信息
86     :param sections_name: 用户名
87     :return: 返回字典用户信息
88     """
89     con = configparser.ConfigParser()
90     con.read(settings.USER_INI, encoding='utf-8')
91     user_dict = {}
92     for i, j in con.items(sections_name):
93         user_dict[i] = j
94     return user_dict
dbapi
  1 #!/usr/bin/env python
  2 #coding=utf-8
  3 __author__ = 'yinjia'
  4 
  5 
  6 import os,sys,time
  7 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
  8 from lib.common import Logger
  9 from lib.user import Users
 10 from lib import common
 11 
 12 
 13 logger = Logger('serverr').getlog()
 14 
 15 
 16 
 17 def client_auth(client_socket,args):
 18 
 19     """
 20     客户端认证
 21     :param client_socket: 客户端socket对象
 22     :param args: 用户发送过来的数据 ex: "auth|test|a7470858e79c282bc2f6adfd831b132672dfd1224c1e78cbf5bcd057"
 23     :return: success:认证成功;user_error:用户名不存在;fail:认证失败
 24     """
 25     recv_data_list = args.split("|")
 26     username = recv_data_list[1]
 27     passwd = recv_data_list[2]
 28     client_user = Users(username)
 29     #判断用户名是否存在
 30     if client_user.check_user():
 31         msg = client_user.load_user_info()
 32         password,totalspace,userspace = msg.strip().split("|")
 33         user_info = "{0}|{1}".format(totalspace, userspace)
 34         #判断密码是否正确
 35         if password == passwd:
 36             auth_status = "success"
 37         else:
 38             auth_status = "fail"
 39     else:
 40         auth_status = "user_error"
 41 
 42     #将认证状态发送给客户端
 43     client_socket.sendall(bytes(auth_status,encoding='utf-8'))
 44     if auth_status == "success":
 45         # 认证成功将用户空间消息发给客户端
 46         client_socket.sendall(bytes(user_info, encoding='utf-8'))
 47     return client_user
 48 
 49 
 50 def cd(client_socket,client_user,ret_data):
 51     """
 52     切换目录路径
 53     :param client_socket: 客户端socket对象
 54     :param client_user: 客户端用户对象
 55     :param ret_data: 接收客户命令消息体  例如:cd|..或cd|test或cd|/test/aa/bb
 56     :return:
 57     """
 58     #获取命令行消息体
 59     cd_folder = ret_data.split("|")[1]
 60     try:
 61         #判断是否当前根目录
 62         if cd_folder == "..":
 63             if client_user.userpath == client_user.homepath:
 64                 sed_msg = "0|{0}".format(os.path.basename(client_user.userpath))
 65             else:
 66                 #返回上一级目录
 67                 client_user.userpath = os.path.dirname(client_user.userpath)
 68                 sed_msg = "1|{0}".format(os.path.basename(client_user.userpath))
 69         elif cd_folder == "." or cd_folder == "":
 70             sed_msg = "3|{0}".format(cd_folder)
 71         else:
 72             #组合路径目录
 73             tmp_path = os.path.join(client_user.userpath, cd_folder)
 74             if os.path.isdir(tmp_path):
 75                 client_user.userpath = tmp_path
 76                 sed_msg = "1|{0}".format(os.path.basename(client_user.userpath))
 77             else:
 78                 # 不是文件夹
 79                 sed_msg = "2|{0}".format(cd_folder)
 80         # 开始发送结果
 81         client_socket.sendall(bytes(sed_msg,encoding='utf-8'))
 82     except Exception as e:
 83         logger.error(e)
 84 
 85 def put(client_socket,client_user,ret_data):
 86     """
 87     上传文件
 88     :param client_socket:
 89     :param client_user:
 90     :param ret_data:
 91     :return:
 92     """
 93     # 初始化上传文件的基本信息
 94     filename = ret_data.split("|")[1]
 95     filesize = int(ret_data.split("|")[2])
 96     filemd5 = ret_data.split("|")[3]
 97     put_folder = client_user.userpath
 98     check_filename = os.path.isfile(os.path.join(put_folder,filename))
 99     save_path = os.path.join(put_folder, filename)
100     fmd5 = common.md5sum(save_path)
101     #不存在文件名,正常传输
102     if not check_filename:
103         client_socket.sendall(bytes("ok",encoding='utf-8'))
104         # 全新的文件的话,更新用户使用空间大小
105         client_user.update_quota(filesize)
106         # 已经接收的文件大小
107         has_recv = 0
108         with open(save_path,'wb') as f:
109             while True:
110                 # 如果文件总大小等于已经接收的文件大小,则退出
111                 if filesize == has_recv:
112                     break
113                 data = client_socket.recv(1024)
114                 f.write(data)
115                 has_recv += len(data)
116     else:
117         #存在文件名条件,做判断分析是否存在断点
118         if fmd5 == filemd5:
119             client_socket.sendall(bytes("full", encoding='utf-8'))
120             # 已经接收的文件大小
121             has_recv = 0
122             with open(save_path, 'wb') as f:
123                 while True:
124                     # 如果文件总大小等于已经接收的文件大小,则退出
125                     if filesize == has_recv:
126                         break
127                     data = client_socket.recv(1024)
128                     f.write(data)
129                     has_recv += len(data)
130         else:
131             #存在断点文件,发起请求续签标志
132             recv_size = os.stat(save_path).st_size
133             ready_status = "{0}|{1}".format("continue", str(recv_size))
134             client_socket.sendall(bytes(ready_status, encoding='utf-8'))
135             # 已经接收的文件大小
136             has_recv = 0
137             with open(save_path, 'wb') as f:
138                 while True:
139                     # 如果文件总大小等于已经接收的文件大小,则退出
140                     if filesize == has_recv:
141                         break
142                     data = client_socket.recv(1024)
143                     f.write(data)
144                     has_recv += len(data)
145 
146 def get(client_socket,client_user,ret_data):
147     """
148     下载文件
149     :param client_socket:
150     :param client_user:
151     :param ret_data:
152     :return:
153     """
154     # 获取文件名
155     filename = ret_data.split("|")[1]
156     # 文件存在吗
157     file = os.path.join(client_user.userpath, filename)
158     if os.path.exists(file):
159         # 先告诉客户端文件存在标识
160         client_socket.send(bytes("1", 'utf8'))
161         # 得到客户端回应
162         client_socket.recv(1024)
163         # 发送文件的基本信息 "filesize|file_name|file_md5"
164         filesize = os.stat(file).st_size
165         file_md5 = common.md5sum(file)
166         sent_data = "{fsize}|{fname}|{fmd5}".format(fsize=str(filesize),
167                                                     fname=filename,
168                                                     fmd5=file_md5)
169         client_socket.sendall(bytes(sent_data, 'utf8'))
170 
171         # 客户端收到ready
172         if str(client_socket.recv(1024), 'utf-8') == "ready":
173             # 开始发送数据了
174             with open(file, 'rb') as f:
175                 new_size = 0
176                 for line in f:
177                     client_socket.sendall(line)
178                     new_size += len(line)
179                     if new_size >= filesize:
180                         break
181     else:
182         # 文件不存在
183         client_socket.send(bytes("0", 'utf8'))
184 
185 
186 def mk(client_socket,client_user,ret_data):
187     """
188     创建目录
189     :param client_socket: 客户端socket对象
190     :param client_user: 客户端用户对象
191     :param ret_data: 接收客户命令消息体  例如:mk|test
192     :return:
193     """
194     mk_folder = ret_data.split("|")[1]
195     if mk_folder:
196         try:
197             folder_path = os.path.join(client_user.homepath, mk_folder)
198             os.makedirs(folder_path)
199             client_socket.sendall(bytes("5000",encoding='utf-8'))
200         except Exception as e:
201             client_socket.sendall(bytes("5001",encoding='utf-8'))
202             logger.error("create directory failure: %s" % e)
203     else:
204         client_socket.sendall(bytes("5002", encoding='utf-8'))
205 
206 def delete(client_socket,client_user,ret_data):
207     """
208     删除目录或文件
209     :param client_socket:客户端socket对象
210     :param client_user:客户端用户对象
211     :param ret_data:接收消息体:样本格式:delete|/test/aa
212     :return:
213     """
214     del_folder = ret_data.split("|")[1]
215     if del_folder:
216         try:
217             #判断文件名是否存在
218             folder_path = os.path.join(client_user.homepath, del_folder)
219             filesize = os.stat(folder_path).st_size
220             if os.path.isfile(folder_path):
221                 os.remove(folder_path)
222                 client_user.update_down_quota(filesize)
223                 sent_data = "{staus}|{fsize}".format(staus="6001",
224                                                     fsize=str(filesize)
225                                                     )
226 
227                 client_socket.sendall(bytes(sent_data, encoding='utf-8'))
228             #判断目录是否存在
229             elif os.path.isdir(folder_path):
230                 os.removedirs(folder_path)
231                 client_socket.sendall(bytes("6000", encoding='utf-8'))
232             else:
233                 #目录或文件名不存在情况,删除失败
234                 client_socket.sendall(bytes("6002", encoding='utf-8'))
235         except Exception as e:
236                 #当前路径目录下不是空目录,不能删除
237                 client_socket.sendall(bytes("6004", encoding='utf-8'))
238                 logger.error("Delete directory or filename failure: %s" % e)
239     else:
240         #命令行后是空白目录
241         client_socket.sendall(bytes("6003", encoding='utf-8'))
242 
243 
244 def ls(client_socket,client_user,ret_data):
245     """
246     显示当前文件目录及文件名
247     :param client_socket: 客户端socket对象
248     :param client_user: 客户端用户对象
249     :param ret_data: 接收消息体样本格式:ls|
250     :return:
251     """
252     check_folder = client_user.userpath
253     #获取用户目录下的文件目录或文件名列表
254     file_list = os.listdir(check_folder)
255     #目录下的文件个数
256     file_count = len(file_list)
257     if file_count > 0:
258         return_list = "{filecount}|".format(filecount=file_count)
259         for rootpath in file_list:
260             file = os.path.join(check_folder,rootpath)
261             stat = os.stat(file)
262             create_time = time.strftime('%Y:%m-%d %X', time.localtime(stat.st_mtime))
263             file_size = stat.st_size
264             if os.path.isfile(file):
265                 return_list += "{ctime}        {fsize}    {fname}
".format(ctime=create_time,
266                                                                             fsize=str(file_size).rjust(10, " "),
267                                                                             fname=rootpath)
268             if os.path.isdir(file):
269                 return_list += "{ctime}  <DIR> {fsize}    {fname}
".format(ctime=create_time,
270                                                                             fsize=str(file_size).rjust(10, " "),
271                                                                             fname=rootpath)
272     else:
273         return_list = "0|"
274 
275     try:
276         # 开始发送信息到客户端
277         # 1 先把结果串的大小发过去
278         str_len = len(return_list.encode("utf-8"))
279         client_socket.sendall(bytes(str(str_len), encoding='utf-8'))
280         # 2 接收客户端 read 标识,防止粘包
281         read_stat = client_socket.recv(1024).decode()
282         if read_stat == "ready":
283             client_socket.sendall(bytes(return_list, encoding='utf-8'))
284         else:
285             logger.error("client send show command,send 'ready' status fail")
286     except Exception as e:
287         logger.error(e)
server
  1 #!/usr/bin/env python
  2 #coding=utf-8
  3 __author__ = 'yinjia'
  4 
  5 
  6 import os,sys
  7 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
  8 from config import settings
  9 from dbhelper import dbapi
 10 from lib.common import Logger
 11 
 12 logger = Logger('user').getlog()
 13 
 14 """
 15 服务端用户信息类
 16 """
 17 
 18 class Users(object):
 19     def __init__(self,username):
 20         self.username = username
 21         self.password = ""
 22         self.totalspace = 0
 23         self.userspace = 0
 24         self.homepath = os.path.join(settings.USER_HOME_FOLDER, self.username)
 25         self.userpath = self.homepath
 26 
 27     def create_user(self):
 28         """
 29         创建用户
 30         :return: True:创建用户成功; False: 创建用户失败
 31         """
 32         args = dict(password=str(self.password), totalspace=str(self.totalspace), userspace=str(self.userspace))
 33         dbapi.AddOption(self.username, **args)
 34         self.__create_folder()
 35 
 36     def del_user(self):
 37         """
 38         删除用户
 39         :return: True;删除用户成功;False: 删除用户失败
 40         """
 41         dbapi.DelSections(self.username)
 42         self.__del_folder()
 43 
 44     def check_user(self):
 45         """
 46         判断用户是否存在
 47         :return:
 48         """
 49         if dbapi.CheckSections(self.username):
 50             return True
 51         return False
 52 
 53 
 54     def load_user_info(self):
 55         """
 56         加载用户信息,赋值给属性
 57         :return:
 58         """
 59         user_info = dbapi.load_info(self.username)
 60         self.password = user_info["password"]
 61         self.totalspace = int(user_info["totalspace"])
 62         self.userspace = int(user_info["userspace"])
 63         msg = "{0}|{1}|{2}".format(self.password, self.totalspace, self.userspace)
 64         return msg
 65 
 66     def __create_folder(self):
 67         """
 68         创建用户的目录
 69         :return:
 70         """
 71         os.mkdir(self.homepath)
 72 
 73     def __del_folder(self):
 74         """
 75         删除用户目录
 76         :return:
 77         """
 78         os.removedirs(self.homepath)
 79 
 80 
 81     def update_quota(self,filesize):
 82         """
 83         更新用户磁盘配额数据
 84         :param filesize: 上传文件大小
 85         :return: True: 更新磁盘配额成功;False:更新磁盘配额失败
 86         """
 87         if dbapi.CheckSections(self.username):
 88             self.userspace += filesize
 89             args = dict(userspace=str(self.userspace))
 90             dbapi.ModifyOption(self.username, **args)
 91             return True
 92         return False
 93 
 94     def update_down_quota(self,filesize):
 95         """
 96         用户删除文件情况,自动减少对应文件大小并更新用户磁盘配额空间
 97         :param filesize:
 98         :return:
 99         """
100         if dbapi.CheckSections(self.username):
101             self.userspace -= filesize
102             args = dict(userspace=str(self.userspace))
103             dbapi.ModifyOption(self.username, **args)
104             return True
105         return False
user

2、客户端

 1 #!/usr/bin/env python
 2 #coding=utf-8
 3 __author__ = 'yinjia'
 4 
 5 import os,sys
 6 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
 7 from config import settings,template,code
 8 from lib import common
 9 from lib.client import client
10 
11 logger = common.Logger('ftpclient').getlog()
12 
13 
14 def run():
15     common.message(template.START_MENU,"INFO")
16     common.message("正在连接服务器 {0}:{1}......".format(settings.FTP_SERVER_IP,settings.FTP_SERVER_PORT),"INFO")
17 
18     #创建对象
19     client_obj = client(settings.FTP_SERVER_IP,settings.FTP_SERVER_PORT)
20     #连接服务器,返回结果
21     conn_result = client_obj.connect()
22     if conn_result == code.CONN_SUCC:
23         common.message("连接成功!", "INFO")
24         #客户端登录
25         login_result = client_obj.login()
26         if login_result:
27             exit_flag = False
28             while not exit_flag:
29                 show_menu = template.LOGINED_MENU.format(client_obj.username,
30                                                          str(int(client_obj.totalspace / 1024 / 1024)),
31                                                          str(int(client_obj.userspace / 1024 / 1024)))
32                 common.message(show_menu,"INFO")
33                 inp_command = common.input_command("[请输入命令]:")
34                 if inp_command == "quit":
35                     exit_flag = True
36                 else:
37                     #获取命令
38                     func = inp_command.split("|")[0]
39                     try:
40                         if hasattr(client, func):
41                             #从模块寻找到函数
42                             target_func = getattr(client, func)
43                             #执行函数
44                             target_func(client_obj, inp_command)
45                         else:
46                             common.message("Client {0} 未找到".format(inp_command), "ERROR")
47                     except Exception as e:
48                         logger.error(e)
49     else:
50         common.message("连接失败!", "ERROR")
ftpclient
  1 #!/usr/bin/env python
  2 #coding=utf-8
  3 __author__ = 'yinjia'
  4 
  5 
  6 import os,sys,socket
  7 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
  8 from config import settings
  9 from config import code
 10 from lib import common
 11 
 12 logger = common.Logger('client').getlog()
 13 
 14 class client(object):
 15     def __init__(self,server_addr, server_port):
 16         self.username =""
 17         self.totalspace = 0
 18         self.userspace = 0
 19         self.client = socket.socket()
 20         self.__server = (server_addr, server_port)
 21 
 22     def connect(self):
 23         """
 24         客户端连接验证
 25         :return: 连接成功返回1000;连接失败返回1001
 26         """
 27         try:
 28             self.client.connect(self.__server)
 29             ret_bytes = self.client.recv(1024)
 30             #接收服务端消息
 31             ret_str = str(ret_bytes, encoding='utf-8')
 32             if ret_str == "OK":
 33                 return code.CONN_SUCC
 34             else:
 35                 return code.CONN_FAIL
 36         except Exception as e:
 37             logger.error(e)
 38 
 39     def check_auth(self,user, passwd):
 40         """
 41         客户端状态发送给服务端验证,并返回结果
 42         :param user: 用户名
 43         :param passwd: 密码
 44         :return:
 45         """
 46         sendmsg = "{cmd}|{user}|{passwd}".format(cmd="auth",
 47                                                  user=user,
 48                                                  passwd=passwd)
 49         self.client.sendall(bytes(sendmsg,encoding='utf-8'))
 50         ret_bytes = self.client.recv(1024)
 51         #获取服务端返回的认证状态信息
 52         auth_info = str(ret_bytes, encoding='utf-8')
 53         if auth_info == "success":
 54             self.username = user
 55             #获取服务端返回用户空间信息
 56             user_info = str(self.client.recv(1024),encoding='utf-8')
 57             self.totalspace = int(user_info.split("|")[0])
 58             self.userspace = int(user_info.split("|")[1])
 59             return code.AUTH_SUCC
 60         if auth_info == "user_error":
 61             return code.AUTH_USER_ERROR
 62         if auth_info == "fail":
 63             return code.AUTH_FAIL
 64 
 65     def login(self):
 66         while True:
 67             username = str(input("请输入用户名:")).strip()
 68             password = str(input("请输入密码:")).strip()
 69             #对密码进行md5加密
 70             password = common.md5(password)
 71             #登录认证
 72             auth_status = self.check_auth(username,password)
 73             if auth_status == code.AUTH_SUCC:
 74                 common.message(">>>>>>>登录成功","INFO")
 75                 return True
 76             elif auth_status == code.AUTH_USER_ERROR:
 77                 common.message(">>>>>>>用户名不存在","ERROR")
 78                 return False
 79             else:
 80                 common.message(">>>>>>>用户名或密码错误!","ERROR")
 81                 return False
 82 
 83     def mk(self,command):
 84         """
 85         创建目录
 86         :param command: 发送命令消息格式;mk|test或mk|/test/yj
 87         :return:
 88         """
 89         #发送命令消息给服务端
 90         self.client.sendall(bytes(command,encoding='utf-8'))
 91         #接收服务端发来的回应消息
 92         mk_msg = str(self.client.recv(1024), encoding='utf-8')
 93         mk_msg = int(mk_msg)
 94         if mk_msg == code.FILE_MK_SUCC:
 95             common.message(">>>>>>>创建目录成功","INFO")
 96         elif mk_msg == code.FILE_MK_FAIL:
 97             common.message(">>>>>>>创建目录失败","ERROR")
 98         else:
 99             common.message(">>>>>>>请输入文件夹名","ERROR")
100 
101     def delete(self,command):
102         """
103         删除目录或文件名
104         :param command: delete|PycharmProjects/untitled/project/FTPv1/FTPServer/upload/admin/test/aa
105         :return:
106         """
107         # 发送命令消息给服务端
108         self.client.sendall(bytes(command, encoding='utf-8'))
109         # 接收服务端发来的回应消息
110         del_msg = str(self.client.recv(1024), encoding='utf-8')
111         reve_status = int(del_msg.split("|")[0])
112         reve_delfilename_fsize = int(del_msg.split("|")[1])
113 
114         if del_msg == code.FOLDER_DEL_SUCC:
115             common.message(">>>>>>>删除目录成功","INFO")
116         elif reve_status == code.FILE_DEL_SUCC:
117             #更新用户空间配额大小
118             self.userspace -= reve_delfilename_fsize
119             common.message(">>>>>>>删除文件名成功","INFO")
120         elif reve_status == code.FILE_DEL_FAIL:
121             common.message(">>>>>>>删除目录或文件名失败","ERROR")
122         elif reve_status == code.FILE_DEL_EMPTY:
123             common.message(">>>>>>>当前目录下不是空目录,无法删除!","ERROR")
124         else:
125             common.message(">>>>>>>命令行请输入需要删除的路径目录或文件名!","ERROR")
126 
127     def cd(self,command):
128         """
129         切换目录路径
130         :param command: cd|.. 或cd|foldername
131         :return: 返回状态信息
132         """
133         # 发送命令消息给服务端
134         self.client.sendall(bytes(command, encoding='utf-8'))
135         # 接收服务端发来的回应消息
136         cd_msg = str(self.client.recv(1024), encoding='utf-8')
137         result_status,result_folder = cd_msg.split("|")
138         if result_status == "0":
139             result_value = "当前是根目录"
140         elif result_status == "1":
141             result_value = "目录已切换到:{0}".format(result_folder)
142         elif result_status == "2":
143             result_value = "切换失败, {0} 不是一个目录".format(result_folder)
144         elif result_status == "3":
145             result_value = "命令无效:{0}".format(result_folder)
146         common.message(result_value,"INFO")
147 
148     def ls(self,*args):
149         """
150         显示客户端的文件列表详细信息
151         :param args:
152         :return: 返回文件列表
153         """
154         try:
155             # 发送命令到服务端
156             self.client.send(bytes("ls|", encoding='utf-8'))
157             # 接收服务端发送结果的大小
158             total_data_len = self.client.recv(1024).decode()
159             # 收到了并发送一个ready标识给服务端
160             self.client.send(bytes("ready", 'utf-8'))
161 
162             # 开始接收数据
163             total_size = int(total_data_len)  # 文件总大小
164             has_recv = 0  # 已经接收的文件大小
165             exec_result = bytes("", 'utf8')
166             while True:
167                 # 如果文件总大小等于已经接收的文件大小,则退出
168                 if total_size == has_recv:
169                     break
170                 data = self.client.recv(1024)
171                 has_recv += len(data)
172                 exec_result += data
173             # 获取结果中文件及文件夹的数量
174             return_result = str(exec_result, 'utf-8')
175             file_count = int(return_result.split("|")[0])
176             if file_count == 0:
177                 return_result = "目前无上传记录"
178             else:
179                 return_result = return_result.split("|")[1]
180             common.message(return_result,"INFO")
181         except Exception as e:
182             logger.error("client ls:{0}".format(e))
183 
184     def put(self,command):
185         """
186         上传文件
187         :param command: put|folderfile
188         :return:
189         """
190         file_name = command.split("|")[1]
191         if os.path.isfile(file_name):
192             filename = os.path.basename(file_name)
193             fsize = os.stat(file_name).st_size
194             fmd5 = common.md5sum(file_name)
195 
196             # 将基本信息发给服务端
197             file_msg = "{cmd}|{file}|{filesize}|{filemd5}".format(cmd='put',
198                                                                   file=filename,
199                                                                   filesize=fsize,
200                                                                   filemd5=fmd5)
201             self.client.send(bytes(file_msg, encoding='utf8'))
202             logger.info("send file info: {0}".format(file_msg))
203             #接收来自服务端数据
204             put_msg = str(self.client.recv(1024),encoding='utf-8')
205             try:
206                 #正常上传文件
207                 if put_msg == "ok":
208                     #判断是否超过用户空间配额
209                     if self.userspace + fsize > self.totalspace:
210                         common.message("用户磁盘空间不足,无法上传文件,请联系管理员!","ERROR")
211                     else:
212                         self.userspace += fsize
213                         new_size = 0
214                         with open(file_name,'rb') as f:
215                             for line in f:
216                                 self.client.sendall(line)
217                                 new_size += len(line)
218                                 # 打印上传进度条
219                                 common.progress_bar(new_size,fsize)
220                                 if new_size >= fsize:
221                                     break
222                 #断点续传文件
223                 if put_msg.split("|")[0] == "continue":
224                     send_size = int(put_msg.split("|")[1])
225                     common.message("服务端存在此文件,但未上传完,开始断点续传......","INFO")
226                     new_size = 0
227                     with open(file_name,'rb') as f:
228                         #用seek来进行文件指针的偏移,实现断点续传的功能
229                         f.seek(send_size)
230                         while fsize - send_size > 1024:
231                             revedata = f.read(1024)
232                             self.client.sendall(revedata)
233                             new_size += len(revedata)
234                             #打印上传进度条
235                             common.progress_bar(new_size, fsize)
236                         else:
237                             revedata = f.read(fsize - send_size)
238                             self.client.sendall(revedata)
239                             # 打印上传进度条
240                             common.progress_bar(new_size, fsize)
241 
242                 #不存在断点文件情况,询问是否覆盖掉原文件
243                 if put_msg == "full":
244                     inp_msg = common.message("服务端存在完整文件,是否覆盖掉原文件[输入y或n]:","INFO")
245                     inp = str(input(inp_msg)).strip().lower()
246                     if inp == "y":
247                         with open(file_name, 'rb') as f:
248                             new_size = 0
249                             for line in f:
250                                 self.client.sendall(line)
251                                 new_size += len(line)
252                                 #打印上传进度条
253                                 common.progress_bar(new_size, fsize)
254                                 if new_size >= fsize:
255                                     break
256                     elif inp == "n":
257                         sys.exit()
258                     else:
259                        common.message("无效命令", "ERROR")
260                 logger.info("upload file<{0}> successful".format(file_name))
261                 common.message("文件上传成功", "INFO")
262             except Exception as e:
263                 logger.error("文件上传失败:{0}".format(e))
264                 common.message("文件上传失败!", "ERROR")
265         else:
266             common.message("文件不存在!", "ERROR")
267 
268     def get(self,command):
269         """
270         下载文件
271         :param command:
272         :return:
273         """
274         return_result = ""
275         # 发送基本信息到服务端 (command,username,file)
276         self.client.send(bytes(command, encoding='utf-8'))
277         # 先接收到命令是否正确标识,1 文件存在, 0 文件不存在
278         ack_by_server = self.client.recv(1024)
279         try:
280             # 文件名错误,当前路径下找不到
281             if str(ack_by_server, encoding='utf-8') == "0":
282                 return_result = "
当前目录下未找到指定的文件,请到存在目录下执行get操作!"
283             else:
284                 # 给服务端回应收到,防止粘包
285                 self.client.send(bytes("ok", 'utf8'))
286 
287                 # 文件存在,开始接收文件基本信息(大小,文件名)
288                 file_info = self.client.recv(1024).decode()
289                 file_size = int(file_info.split("|")[0])
290                 file_name = file_info.split("|")[1]
291                 file_md5 = file_info.split("|")[2]
292 
293                 # 2 发送 ready 标识,准备开始接收文件
294                 self.client.send(bytes("ready", 'utf8'))
295 
296                 # 3 开始接收数据了
297                 has_recv = 0
298                 with open(os.path.join(settings.DOWNLOAD_FILE_PATH, file_name), 'wb') as f:
299                     while True:
300                         # 如果文件总大小等于已经接收的文件大小,则退出
301                         if file_size == has_recv:
302                             break
303                         data = self.client.recv(1024)
304                         f.write(data)
305                         has_recv += len(data)
306                         # 打印下载进度条
307                         common.progress_bar(has_recv, file_size)
308                 return_result = "
文件下载成功"
309                 logger.info("download file<{0}> from server successful".format(file_name))
310                 # md5文件验证
311                 check_md5 = common.md5sum(os.path.join(settings.DOWNLOAD_FILE_PATH, file_name))
312                 if check_md5 == file_md5:
313                     logger.info("md5 check for file<{0}> succ!".format(file_name))
314                     return_result += ", MD5 验证成功! "
315                 else:
316                     return_result += ", MD5 验证文件不匹配! "
317             common.message(return_result,"INFO")
318         except Exception as e:
319             logger.error(e)
client
  1 #!/usr/bin/env python
  2 #coding=utf-8
  3 __author__ = 'yinjia'
  4 
  5 
  6 import os,sys,logging,hashlib,time
  7 sys.path.append(os.path.dirname(os.path.dirname(__file__)))
  8 from config import settings
  9 
 10 class Logger(object):
 11     """
 12     日志记录,写入指定日志文件
 13     """
 14     def __init__(self,logger):
 15 
 16         create_time = time.strftime('%Y-%m-%d %H:%M:%S')
 17         format = '[%(name)s]:[%(asctime)s] [%(filename)s|%(funcName)s] [line:%(lineno)d] %(levelname)-8s: %(message)s'
 18 
 19         # 创建一个logger
 20         self.logger = logging.getLogger(logger)
 21         self.logger.setLevel(logging.INFO)
 22 
 23         # 创建一个handler,用于写入日志文件
 24         fp = logging.FileHandler(settings.LOGS)
 25 
 26         # 定义handler的输出格式formatter
 27         fpmatter = logging.Formatter(format)
 28         fp.setFormatter(fpmatter)
 29 
 30         # 给logging添加handler
 31         self.logger.addHandler(fp)
 32 
 33     def getlog(self):
 34         return self.logger
 35 
 36 
 37 def md5(arg):
 38     """
 39     密码进行md5加密
 40     :param arg: 用户的密码
 41     :return: 返回进行加密后的密码
 42     """
 43     result = hashlib.md5()
 44     result.update(arg.encode())
 45     return result.hexdigest()
 46 
 47 
 48 def md5sum(filename):
 49     """
 50     用于获取文件的md5值
 51     :param filename: 文件名
 52     :return: MD5码
 53     """
 54     if not os.path.isfile(filename):  # 如果校验md5的文件不是文件,返回空
 55         return False
 56     myhash = hashlib.md5()
 57     f = open(filename, 'rb')
 58     while True:
 59         b = f.read(1024)
 60         if not b:
 61             break
 62         myhash.update(b)
 63     f.close()
 64     return myhash.hexdigest()
 65 
 66 def message(msg,type):
 67     """
 68     根据不同的消息类型,打印出消息内容以不同的颜色显示
 69     :param msg: 消息内容
 70     :param type: 消息类型
 71     :return: 返回格式化后的消息内容
 72     """
 73     if type == "CRITICAL":
 74         show_msg = "
33[1;33m{0}33[0m
".format(msg)
 75     elif type == "ERROR":
 76         show_msg = "
33[1;31m{0}33[0m
".format(msg)
 77     elif type == "WARNING":
 78         show_msg = "
33[1;32m{0}33[0m
".format(msg)
 79     elif type == "INFO":
 80         show_msg = "
33[1;36m{0}33[0m
".format(msg)
 81     else:
 82         show_msg = "
{0}
".format(msg)
 83     print(show_msg)
 84 
 85 def progress_bar(cache, totalsize):
 86     """
 87     打印进度条
 88     :param cache: 缓存字节大小
 89     :param totalsize: 文件总共字节
 90     :return:
 91     """
 92     ret = cache / totalsize
 93     num = int(ret * 100)
 94     view = '
%d%% |%s' % (num, num * "*")
 95     sys.stdout.write(view)
 96     sys.stdout.flush()
 97 
 98 def input_command(msg):
 99     flag = False
100     while not flag:
101         command_list = ["put","get","ls","cd","mk","delete","quit"]
102         command_inp = input(msg).strip()
103         if command_inp == "ls":
104             return_command = "{0}|".format(command_inp)
105             flag = True
106         elif command_inp == "quit":
107             return_command = command_inp
108             flag = True
109         else:
110             if command_inp.count("|") != 1:
111                 message("输入命令不合法!","ERROR")
112             else:
113                 #获取命令
114                 cmd,args = command_inp.strip().lower().split("|")
115                 if cmd not in command_list:
116                     message("输入命令不合法!", "ERROR")
117                 else:
118                     return_command = "{0}|{1}".format(cmd, args)
119                     flag = True
120     return return_command
common
  •  部分效果展示图

原文地址:https://www.cnblogs.com/yinjia/p/8683173.html