module03-2-简单ftp

需求


  • 用户加密认证
  • 允许同时多用户登录
  • 每个用户有自己的家目录 ,且只能访问自己的家目录
  • 对用户进行磁盘配额,每个用户的可用空间不同
  • 允许用户在ftp server上随意切换目录
  • 允许用户查看当前目录下文件
  • 允许上传和下载文件,保证文件一致性
  • 文件传输过程中显示进度条
  • 附加功能:支持文件的断点续传

目录结构


ftp_client
    ├ bin   # 执行文件目录
    |   └ ftp_client.py     # ftp客户端执行程序 
    ├ conf  # 配置文件目录
    |   └ setting.py        # 配置文件。目前主要内容为服务端地址
    ├ core  # 程序核心代码位置
    |   └ main.py           # 主逻辑交互程序    
    └ ftpdownload   # 下载文件存储目录
    
ftp_server
    ├ bin   # 执行文件目录
    |   └ ftp_server.py     # ftp服务端执行程序 
    ├ conf  # 配置文件目录
    |   └ setting.py        # 配置文件。目前主要内容为服务端地址
    ├ core  # 程序核心代码位置
    |   ├ main.py           # 主逻辑交互程序
    |   └ init_user.py      # 用来 初始化用户/创建用户
    ├ log   # 日志文件存储目录
    ├ userinfo      # 用户信息数据库
    └ userstorage   # 用户空间

代码


 

FTP 服务端

1 import os,sys
2 
3 BasePath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4 sys.path.insert(0,BasePath)
5 
6 from core import main
7 main.main()
ftp_server.py
server_addr = {
    'ip':'127.0.0.1',
    'port':12345
}
setting.py
 1 import hashlib,pickle,os
 2 
 3 BasePath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 4 userinfo_dir = os.path.join(BasePath,'userinfo')
 5 userstorage_dir = os.path.join(BasePath,'userstorage')
 6 
 7 print('\nYou can create some users for testing here...')
 8 
 9 while 1:
10     name = input('Name: ').strip()
11     pwd = input('Password: ').strip()
12 
13     m_name = hashlib.md5()
14     m_name.update(name.encode())
15     name_md5 = m_name.hexdigest()
16 
17     m_pwd = hashlib.md5()
18     m_pwd.update(pwd.encode())
19     pwd_md5 = m_pwd.hexdigest()
20 
21     root = os.path.join(userstorage_dir,name)
22 
23     while 1:
24         quota = input('quota: ').strip()
25         if quota.isdigit() or quota == '':
26             quota = int(quota) if quota else 1024000000
27             break
28         else:
29             continue
30 
31     user_info = {
32         'name':name,
33         'pwd':pwd_md5,
34         'root':root,
35         'quota':quota,
36         'space_size':0,  # 记录已用空间大小,暂未使用
37         'files_md5':{},  # 记录已上传文件的md5
38         'put_progress':{}  # 记录未上传完毕文件的进度
39     }
40 
41     user_info_path = os.path.join(userinfo_dir,name)
42     with open(user_info_path,'wb') as f:
43         pickle.dump(user_info,f)
44 
45     user_storage_path = os.path.join(userstorage_dir,name)
46     if not os.path.isdir(user_storage_path):
47         os.mkdir(user_storage_path)
48 
49     print('''
50 User %s has be created!
51 %s's database file is \033[1;33m%s\033[0m
52 %s's storage_directory is \033[1;33m%s\033[0m
53 %s's disk quota is %d Bytes
54 '''%(name,name,user_info_path,name,user_storage_path,name,quota))
55 
56     continue_flag = input('Please press \'\033[1;31mq\033[0m\' to quit or other key to create another user').strip()
57 
58     if continue_flag == 'q':
59         break
init_user.py
  1 #! /usr/bin/env python3
  2 # -*- encoding:utf-8 -*-
  3 # Author:Jailly
  4 
  5 import socketserver
  6 import os
  7 import sys
  8 import json
  9 import pickle
 10 import re
 11 import subprocess
 12 import locale
 13 import hashlib
 14 import logging
 15 from logging import handlers
 16 
 17 BasePath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 18 sys.path.insert(0,BasePath)
 19 
 20 from conf import setting
 21 
 22 userinfo_dir = os.path.join(BasePath,'userinfo')
 23 userstorage_dir = os.path.join(BasePath,'userstorage')
 24 sys_encode = locale.getdefaultlocale()[1]
 25 
 26 logger = logging.getLogger()
 27 logger.setLevel(10)
 28 
 29 log_path = os.path.join(os.path.join(BasePath,'log'),'ftpserver.log')
 30 tfh = handlers.TimedRotatingFileHandler(log_path,when='midnight',backupCount=10,encoding='utf-8')
 31 tfh.setLevel(20)
 32 formatter = logging.Formatter('%(levelname)s:%(message)s.[%(asctime)s]',datefmt=r'%m/%d/%Y %H:%M:%S')
 33 
 34 tfh.setFormatter(formatter)
 35 logger.addHandler(tfh)
 36 
 37 
 38 class MyTCPHandler(socketserver.BaseRequestHandler):
 39     ''' handle the request from client'''
 40 
 41     def __get_user_info(self,name):
 42         '''
 43         get user's information from the file related
 44         :param name: user name
 45         :return: user information
 46         '''
 47 
 48         user_info_file_path = os.path.join(userinfo_dir, name)
 49 
 50         with open(user_info_file_path,'rb') as f:
 51             user_info = pickle.load(f)
 52 
 53         return user_info
 54 
 55 
 56     def __write_user_info(self,user_info):
 57 
 58         user_info_file_path = os.path.join(userinfo_dir, self.user_name)
 59 
 60         with open(user_info_file_path,'wb') as f:
 61             pickle.dump(user_info,f)
 62 
 63 
 64     def authenticate(self):
 65         '''
 66         handle the authentication and return the result code 
 67         :param:
 68         :return: authentication result code:'0' means failure,'1' means sucess
 69         '''
 70 
 71         while 1:
 72 
 73             auth_json = self.request.recv(8192).decode('utf-8')
 74             auth_info = json.loads(auth_json)
 75 
 76             recv_name = auth_info['name']
 77             recv_pwd_md5 = auth_info['pwd']
 78 
 79             if recv_name in os.listdir(userinfo_dir):
 80                 user_info = self.__get_user_info(recv_name)
 81 
 82                 if recv_pwd_md5 == user_info['pwd']:
 83                     self.request.send(b'0')
 84                     logger.info('User %s logins succesfully,client adress is %s'%(recv_name,str(self.client_address)))
 85                     return user_info
 86                 else:
 87                     self.request.send(b'2')
 88 
 89             else:
 90                 self.request.send(b'1')
 91 
 92 
 93     def __replace_path(self,match):
 94         ''' It can only be called by re.sub() in method 'replace_args' '''
 95         if match.group().startswith(os.sep):
 96             return os.path.join(self.user_root,match.group())
 97         else:
 98             return os.path.join(self.user_current_path,match.group())
 99 
100     def __replace_args(self,cmd,args_input):
101         '''
102         Replace file path in arguments inputed.
103         It can only be used in method that handle the command with file_path but 'cd'
104         :return: real_instructions: instructions that has been replaced
105         '''
106 
107         real_args = re.sub(r'((?<=\s)|^)([^-].*)', self.__replace_path, args_input) \
108             if re.search(r'((?<=\s)|^)([^-].*)', args_input) else ' '.join([args_input, self.user_current_path])
109         real_instructions = ' '.join([cmd, real_args])
110 
111         return real_instructions
112 
113 
114     # def __replace_res_con(self,res_con):
115     #     '''
116     #     replace the absolute path of user root on windows
117     #     :param res_con: result of cmd
118     #     :return:
119     #     '''
120     #
121     #     res_con = res_con.replace(self.user_root, '')
122     #     con_list = re.split(r'%s\s*'%os.linesep,res_con)
123     #     new_con_list = []
124     #     for i in range(len(con_list)):
125     #         if i>0:
126     #             temp_line = ''.join([con_list[i-1],con_list[i]])
127     #             if re.search(self.user_root,temp_line):
128     #                 pass
129 
130 
131     def cmd_base(self,*args):
132         '''
133         execute command that its arguments contains path, and send result to client
134         :param: args[1]: cmd
135         :param: args[2]; args_input
136         :return:
137         '''
138 
139         instructions = self.__replace_args(args[1], args[2])
140 
141         if os.name == 'posix':
142             res = subprocess.Popen(instructions,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
143         elif os.name == 'nt':
144             instructions_list = [r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe','-ExecutionPolicy','RemoteSigned']
145             instructions_list.extend(instructions.split())
146             # print(instructions_list)
147             res = subprocess.Popen(instructions_list,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
148         else:
149             # 待验证
150             res = subprocess.Popen(instructions, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
151 
152         out, err = res.communicate()
153         res_con = err.decode(sys_encode) if res.wait() else out.decode(sys_encode)
154         # windows系统powershell的某些命令输出中包含用户存储空间的绝对路径,需替换 或 删除相关字段!!!
155         res_con = re.split(r'(?:\r\n){3}',res_con,maxsplit=1)[1] if len(re.split(r'(\r\n){3}',res_con,maxsplit=1)) > 1 \
156             else res_con
157         # print(res_con)
158 
159         # 发送json格式可以避免命令执行成功但无返回,导致的发送为空的情况;且兼具了扩展性
160         res_cmd = {
161             'res_con':res_con
162         }
163 
164         res_json = json.dumps(res_cmd)
165         # print(res_json)
166         # 发送命令结果
167         self.send_result(res_json.encode())
168 
169 
170     def send_result(self,res_json):
171         '''
172         send the result of command to client
173         :param cmd_json: result of command,it's a byte-like object
174         :return:
175         '''
176 
177         len_res_json = len(res_json)
178         self.request.send(str(len_res_json).encode())
179         self.request.recv(8192)  # block to avoid packet splicing(s:2)
180         self.request.sendall(res_json)
181 
182 
183     def cmd_ls(self,*args):
184         self.cmd_base(*args)
185 
186 
187     def cmd_pwd(self,*args):
188         '''
189         execute 'pwd' command and send the result to client
190         :param: args[1]: cmd
191         :param: args[2]; args_input
192         :return:
193         '''
194 
195         res_con = self.user_current_path.replace(self.user_root,'')
196         res_con = res_con if res_con else os.sep
197         res_cmd = {'res_con':res_con}
198         res_json = json.dumps(res_cmd)
199         self.send_result(res_json.encode())
200 
201 
202     def cmd_cd(self,*args):
203         '''
204         execute 'cd' command(set 'self.user_current_path' actually) and send result to client
205         :param: args[1]: cmd
206         :param: args[2]; args_input
207         :return:
208         '''
209 
210         file_path_input = re.search(r'((?<=\s)|^)([^-].*)',args[2]).group()  # 仅适用于以 ‘-’ 做参数前缀的情况
211         last_current_path = self.user_current_path
212         if file_path_input.startswith(os.sep):
213             file_path_input = file_path_input.replace(os.sep,'',1)
214             self.user_current_path = os.path.join(self.user_root,file_path_input)
215         elif file_path_input == '.':
216             pass
217         elif file_path_input == '..':
218             self.user_current_path = os.path.dirname(self.user_current_path)
219         else:
220             self.user_current_path = os.path.join(self.user_current_path,file_path_input)
221 
222         if os.path.isdir(self.user_current_path):
223             if self.user_current_path.startswith(self.user_root):
224                 res_cmd = {'res_con': 0}  # 0 means 'Execute command successfully'
225             else:
226                 self.user_current_path = self.user_root
227                 res_cmd = {'res_con': 2} # 2 means 'You do not have permission to access the directory'
228 
229         else:
230             self.user_current_path = last_current_path
231             res_cmd = {'res_con': 1}  # 1 means 'Not such directory'
232 
233         res_json = json.dumps(res_cmd)
234 
235         self.send_result(res_json.encode())
236 
237 
238     def cmd_mkdir(self,*args):
239         self.cmd_base(*args)
240 
241 
242     def cmd_rmdir(self,*args):
243         self.cmd_base(*args)
244 
245 
246     def cmd_rm(self,*args):
247         '''
248         execute 'rm' command , and send result to client
249         :param: args[1]: cmd
250         :param: args[2]; args_input
251         :return:
252         '''
253         self.cmd_base(*args)
254 
255         file_path_input = re.search(r'((?<=\s)|^)([^-].*)',args[2]).group()
256         user_info = self.__get_user_info(self.user_name)
257 
258         # 同时删除其md5记录,上传进度记录
259         if file_path_input in user_info['files_md5']:
260             del user_info['files_md5'][file_path_input]
261 
262         if file_path_input in user_info['put_progress']:
263             del user_info['files_md5'][file_path_input]
264 
265 
266     def __get_dir_size(self,dir):
267         '''
268         obtain the target directory's size
269         :param dir:target directory's absolute path
270         :return:target directory's size
271         '''
272 
273         walks = os.walk(dir)
274         size = 0
275 
276         for walk in walks:
277             for each_file_relative_path in walk[2]:
278                 each_file_absolute_path = os.path.join(walk[0],each_file_relative_path)
279                 size += os.stat(each_file_absolute_path).st_size
280 
281         return size
282 
283 
284     def __rename_file(self,path,times=1):
285         '''
286         rename file in the form of 'file name + (n)' recursively until its name is different from all files in current directory
287         :param path:the file's path
288         :param times:the times of renaming file
289         :return:
290         '''
291 
292         new_path = path+'(%d)'%times
293         if os.path.isfile(new_path):
294             times += 1
295             return self.__rename_file(path,times)
296         else:
297             return new_path
298 
299 
300     def cmd_put(self,*args):
301         '''
302         receive the file that the client uploads
303         :param args[2]:the path on client of file that was been putted
304         :return:
305         '''
306 
307         self.request.send(b'0')  # cooperate to finish client's block action (c:1)
308         filesize = int(self.request.recv(1024).decode('utf-8'))
309         filename = args[2]
310 
311         # 计算空间是否充足时,要考虑未上传完毕的文件,断点续传时还需再上传多少大小,而不是简单地将现有空间用量 加 待上传文件大小
312         user_info = self.__get_user_info(self.user_name)
313         # print(user_info)
314         file_path = os.path.join(self.user_current_path, filename)
315         has_uploaded_size = user_info['put_progress'][file_path] if file_path in user_info['put_progress'] else 0
316         need_increase_size = filesize - has_uploaded_size
317 
318         # 磁盘配额的两种方案:①每次上传,由服务端实时计算可用空间;②上传文件后,将文件大小加到用户当前磁盘空间上,并将新数值记录为当前用户磁盘空间,
319         # 记录到用户配置文件中,每次上传前读取配置文件中的当前磁盘空间大小,计算用户空间是否充足。这里采用方案①
320         user_current_space_size = self.__get_dir_size(self.user_root)
321         user_expeted_space_size = user_current_space_size + need_increase_size
322 
323         # 告知客户端磁盘空间是否充足:1:不足;2:充足
324         if user_expeted_space_size < self.user_info['quota']:
325             self.request.send(b'0')  # tell client if the space left is enough
326 
327             if os.path.isfile(file_path):
328                 file_path = self.__rename_file(file_path)
329 
330             put_progress = user_info['put_progress'][file_path] if file_path in user_info['put_progress'] else 0
331 
332             self.request.recv(1024) # block to avoid packet splicing(s:3)
333             self.request.send(str(put_progress).encode())
334 
335             m = hashlib.md5()
336             try:
337                 with open(''.join([file_path,'.uploading']), 'a+b') as f:
338                     f.seek(0,0)  # a模式下,指针默认在文件末尾,需先将指针定位到文件头部
339                     m.update(f.read())
340                     while put_progress < (filesize - 8192):
341                         accepting_data = self.request.recv(8192)
342                         m.update(accepting_data)
343                         put_progress += len(accepting_data)
344                         f.write(accepting_data)
345 
346                     while put_progress < filesize:
347                         buffersize = filesize - put_progress
348                         accepting_data = self.request.recv(buffersize)
349                         m.update(accepting_data)
350                         put_progress += len(accepting_data)
351                         f.write(accepting_data)
352 
353             except Exception as e:
354                 print(e)
355                 print('\033[1;31mUploading from %s was been interrupted\033[0m' % (str(self.client_address)))
356 
357                 self.break_flag = 1 # 退出循环,否则IO缓冲区中未接收的数据会又发被handle()中本应接收指令的recv所接收
358 
359                 logger.warning('Uploading the file \'%s\' by user %s interrupted'%(file_path,self.user_name))
360 
361             else:
362                 # 上传完成后去掉文件名中的'.uploading'
363                 os.rename(''.join([file_path,'.uploading']),file_path)
364 
365                 md5_from_client = self.request.recv(1024).decode('utf-8')
366                 # print('md5_from_client: ',md5_from_client)
367                 # print('md5_from_server: ',m.hexdigest())
368                 # 告知客户端完整性验证是否成功:b'0':成功;b'1':失败
369                 self.request.send(b'0' if md5_from_client == m.hexdigest() else b'1')
370 
371                 user_info['files_md5'][file_path]  = m.hexdigest()
372                 self.__write_user_info(user_info)
373 
374                 logger.info('User %s uploads the file \'%s\''%(self.user_name,file_path))
375 
376             finally:
377                 user_info = self.__get_user_info(self.user_name)
378                 # 进度与文件大小相等,则删除上传进度的记录,否则记录上传进度
379                 if put_progress != filesize:
380                     user_info['put_progress'][file_path] = put_progress
381                 else:
382                     if file_path in user_info['put_progress']:
383                         del user_info['put_progress'][file_path]
384 
385                 self.__write_user_info(user_info)
386 
387         else:
388             self.request.send(b'1')
389 
390 
391     def cmd_get(self,*args):
392         '''
393         send the file that the client downloads
394         :param args[2]:the path on client of file that was been putted
395         :return:
396         '''
397 
398         filename = args[2]
399         if filename.startswith(os.sep):
400             filename = filename.replace(os.sep,'',1)
401             file_path = os.path.join(self.user_root,filename)
402         else:
403             file_path = os.path.join(self.user_current_path, filename)
404 
405         if os.path.isfile(file_path):
406             self.request.send(b'0')  # indicate if the file exist:b'0' means yes,b'1' means no
407 
408             get_progress = int(self.request.recv(1024).decode('utf-8'))
409             # print('get_progress: ',get_progress)
410             filesize = os.stat(file_path).st_size
411             self.request.send(str(filesize).encode())
412 
413             try:
414                 with open(file_path,'br') as f:
415                     f.seek(get_progress,0)
416                     for line in f:
417                         self.request.send(line)
418             except Exception as e:
419                 print(e)
420                 print('Connection with %s is closed'%str(self.client_address))
421                 self.break_flag = 1
422 
423                 logger.warning('Dloading the file \'%s\' by user %s interrupted'%(file_path,self.user_name))
424 
425             else:
426                 user_info = self.__get_user_info(self.user_name)
427                 file_md5 = user_info['files_md5'][file_path] if file_path in user_info['files_md5'] else '0'
428                 print(user_info)
429                 self.request.send(file_md5.encode())
430 
431                 logger.info('User %s downloads the file \'%s\''%(self.user_name,file_path))
432 
433             finally:
434                 pass
435 
436         else:
437             self.request.send(b'1')
438 
439 
440     def cmd_exit(self,*args):
441         self.break_flag = 1
442         self.request.close()
443 
444 
445     def handle(self):
446         '''method for handling request'''
447 
448         try:
449             self.break_flag = 0
450 
451             self.user_info = self.authenticate()
452 
453             self.user_name = self.user_info['name']
454             # self.user_info_file_path = os.path.join(userinfo_dir, self.user_name)  # 验证时也会调用,那时还不存在self.user_name
455             self.user_root = os.path.join(userstorage_dir,self.user_name)
456             self.user_current_path = self.user_root
457 
458             while 1:
459                 instructions = self.request.recv(1024)
460                 # print('instructions_byets:',instructions)
461                 instructions = instructions.decode('utf-8')
462                 # print('instructions_str:',instructions)
463 
464                 cmd = instructions.split(maxsplit = 1)[0]
465                 # print(cmd)
466                 args_input = instructions.split(maxsplit = 1)[1] if len(instructions.split()) > 1 else ''
467                 # print(args_input)
468 
469                 if hasattr(self,''.join(['cmd_',cmd])):
470                     # print('cmd found!')
471                     func = getattr(self,''.join(['cmd_',cmd]))
472                     func(self,cmd,args_input)
473 
474                     if self.break_flag == 1:
475                         break
476 
477                 else:
478                     print('%s:command not found'%cmd)
479 
480         except ConnectionResetError:
481             print('\033[1;31mThe connection with %s is closed\033[0m'%(str(self.client_address)))
482 
483             logger.info('The connection with %s is closed'%(str(self.client_address)))
484 
485         except Exception as e:
486             print(e)
487             print('Connection with %s is closed'%str(self.client_address))
488 
489             logger.error('Detected an error occurred: %s'%e)
490 
491 
492 def main():
493     ip = setting.server_addr['ip']
494     port = setting.server_addr['port']
495     myserver = socketserver.ThreadingTCPServer((ip,port),MyTCPHandler)
496 
497     myserver.serve_forever()
498 
499 
500 if __name__ == "__main__":
501     main()
main.py

 

FTP 客户端

1 import os,sys
2 
3 BasePath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4 sys.path.insert(0,BasePath)
5 
6 from core import main
7 main.main()
ftp_client.py
1 server_addr = {
2     'ip':'127.0.0.1',
3     'port':12345
4 }
setting.py
  1 #! /usr/bin/env python3
  2 # -*- encoding:utf-8 -*-
  3 # Author:Jailly
  4 
  5 import socket,os,sys,hashlib,json,time,pickle
  6 
  7 BasePath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  8 sys.path.insert(0,BasePath)
  9 
 10 download_path = os.path.join(BasePath,'ftpdownload')
 11 conf_path = os.path.join(BasePath,'conf')
 12 
 13 from conf import setting
 14 
 15 class FtpClient(object):
 16     ''' handle client's socket creating , connecting with server and interactions'''
 17 
 18     def __init__(self):
 19         self.socket = socket.socket()
 20 
 21 
 22     def help(self):
 23         '''show command that can be used by ftp-client'''
 24 
 25         msg = '''You can use the following command:
 26 \033[1;31mls\033[0m [opt] [file]|[directory]       
 27 \033[1;31mpwd\033[0m
 28 \033[1;31mcd\033[0m [opt] [directory]
 29 \033[1;31mmkdir\033[0m [opt] [directory]
 30 \033[1;31mrmdir\033[0m [opt] [directory]
 31 \033[1;31mrm\033[0m [opt] [directory]
 32 \033[1;31mput\033[0m [file]
 33 \033[1;31mget\033[0m [file]
 34 exit
 35         '''
 36         print(msg)
 37 
 38 
 39     def connect(self,ip,port):
 40         '''
 41         connet with ftp-server
 42         :param: ip: ftp server's ip
 43         :param: port: ftp server's port
 44         '''
 45 
 46         self.socket.connect((ip,port))
 47 
 48 
 49     def get_auth_result(self):
 50         '''
 51         send user's anthentication information and receive authentication result from server
 52         :param:
 53         :return: auth_flag: authentication result code:'0' means failure,'1' means success
 54         :return: name_md5: md5 value of name inputed
 55         :return: name: name inputed
 56         '''
 57 
 58         name = input('user:').strip()
 59         pwd = input('password:').strip()
 60 
 61         m_pwd = hashlib.md5()
 62         m_pwd.update(pwd.encode())
 63         pwd_md5 = m_pwd.hexdigest()
 64 
 65         auth_info = {
 66             'name':name,
 67             'pwd':pwd_md5
 68         }
 69 
 70         auth_json = json.dumps(auth_info)
 71 
 72         self.socket.send(auth_json.encode('utf-8'))
 73 
 74         auth_flag = int(self.socket.recv(1024).decode('utf-8'))
 75         return auth_flag,name
 76 
 77 
 78     def authenticate(self):
 79         '''
 80         just for authentication
 81         :return: 
 82         '''
 83 
 84         auth_times = {}
 85         while 1:
 86             auth_flag,name = self.get_auth_result()
 87 
 88             if auth_flag:
 89                 if auth_flag == 1:
 90                     print('\033[1;31mUser does not exists!\033[0m')
 91                 else:
 92                     auth_times.setdefault(name,1)
 93                     auth_times[name] += 1
 94 
 95                     if auth_times[name] == 3:
 96                         print('\033[1;31mInput wrong password of user \'%s\' more than 3 times, \
 97 The connection will be disconnected in 2 seconds\033[0m'%name,end='')
 98 
 99                         for i in range(3):
100                             sys.stdout.write('.')
101                             sys.stdout.flush()
102                             time.sleep(0.5)
103 
104                         self.socket.close()
105                         exit()
106                     else:
107                         print('\033[1;31mIncorrect password!\033[0m')
108 
109             else:
110                 self.user_name = name
111                 self.local_user_info_path = os.path.join(conf_path,self.user_name)
112                 print('\033[1;33mHi,%s!\nWelcome to Jailly\'s Ftpserver\033[0m'%name)
113                 break
114 
115 
116     def cmd_base(self,*args,only_get_result = False):
117         '''
118         receive return of command from ftp_server and show it
119         :param: args[1]:instructions
120         :param: only_get_result:False -> just return res_con; True -> print res_con
121         :return:
122         '''
123 
124         # cmd = args[0].split(maxsplit=1)[0]
125         # args_input = args[0].split(maxsplit=1)[1] if len(args[0].split()) > 1 else ''
126 
127         self.socket.send(args[1].encode('utf-8'))
128 
129         len_res_json = int(self.socket.recv(1024).decode('utf-8'))
130         # print(len_res_json)
131         self.socket.send(b'0')  # cooperate to finish server's block action(s:2)
132 
133         accepted_len = 0
134         res_json = b''
135         while accepted_len != len_res_json:
136             accepting_data = self.socket.recv(8192)
137             accepted_len += len(accepting_data)
138             res_json += accepting_data
139 
140         res_cmd = json.loads(res_json.decode('utf-8'))
141         res_con = res_cmd['res_con']
142 
143         if only_get_result:
144             return res_con
145 
146         print(res_con)
147 
148 
149     def cmd_ls(self,*args):
150         self.cmd_base(*args)
151 
152 
153     def cmd_pwd(self,*args):
154         self.cmd_base(*args)
155 
156 
157     def cmd_cd(self,*args):
158         res_con = self.cmd_base(*args,only_get_result=True)
159 
160         if res_con == 1:
161             print('\033[1;31mNot such directory\033[0m')
162         elif res_con == 2:
163             print('\033[1;31mYou do not have permission to access this directory\033[0m')
164 
165 
166     def cmd_mkdir(self,*args):
167         self.cmd_base(*args)
168 
169 
170     def cmd_rmdir(self,*args):
171         self.cmd_base(*args)
172 
173 
174     def cmd_rm(self,*args):
175         self.cmd_base(*args)
176 
177 
178     def cmd_put(self,*args):
179         '''
180         upload the file specified
181         :param args[1]: instructions
182         :return:
183         进度条,断点续传,配额,完整性验证
184         '''
185 
186         file_path = args[1].split(maxsplit=1)[1] if len(args[1].split()) > 1 else ''
187         filename = os.path.basename(file_path)
188         if os.path.isfile(file_path):
189 
190             self.socket.send((' '.join(['put',filename])).encode('utf-8'))
191             self.socket.recv(1024)  # 阻塞,以避免"发送指令"与"发送文件长度"2次之间的粘包(c:1)
192 
193             filesize = os.stat(file_path).st_size
194             self.socket.send(str(filesize).encode())
195 
196             # 接收服务端发来的确认。enough为1:用户空间不足;enough为0:用户空间充足
197             enough = self.socket.recv(1024)
198             if enough == b'1':
199                 print('\033[1;31mYour available space is not enough,nothing to do\033[0m')
200                 return
201 
202             else:
203                 self.socket.send(b'0')  # block to avoid packet splicing(s:3)
204                 sent_size = int(self.socket.recv(1024).decode('utf-8'))
205                 percentage_last_sent_file = sent_size*100//filesize
206                 m = hashlib.md5()# md5 object for the file putted
207                 print('Putting \'%s\':'%filename,end=' ')
208                 send_start_time = time.time()
209 
210                 try:
211                     with open(file_path,'rb') as f:
212                         m.update(f.read(sent_size))
213                         sys.stdout.write('#'*percentage_last_sent_file)
214                         sys.stdout.flush()
215 
216                         for line in f:
217                             self.socket.send(line)
218                             m.update(line)
219 
220                             # 打印进度条
221                             sent_size += len(line)
222                             percentage_sent_file = sent_size*100//filesize
223                             num_bar_unit = percentage_sent_file - percentage_last_sent_file
224 
225                             if num_bar_unit:
226                                 sys.stdout.write('#'*num_bar_unit)
227                                 sys.stdout.flush()
228                                 percentage_last_sent_file = percentage_sent_file
229 
230                 except Exception as e:
231                     print(e)
232                     print('Transfer interrupted')
233                 else:
234                     self.socket.send(m.hexdigest().encode('utf-8'))
235                     integrity_confirm = self.socket.recv(1024)
236 
237                     send_end_time = time.time()
238                     upload_speed = filesize/1000/(send_end_time-send_start_time)
239 
240                     print('\nTransfer completed.\nFile integrity authentication %s\nUpload speed:%.1d k/s'
241                           %('succeeded' if integrity_confirm == b'0' else 'failed',upload_speed))
242 
243 
244         else:
245             print('File \'\033[1;31m%s\033[0m\' does not exists'%filename)
246 
247 
248     def __rename_file(self, path, times=1):
249         '''
250         rename file in the form of 'file name + (n)' recursively until its name is different from all files in current directory
251         :param path:the file's path
252         :param times:the times of renaming file
253         :return:
254         '''
255 
256         new_path = path + '(%d)' % times
257         if os.path.isfile(new_path):
258             times += 1
259             return self.__rename_file(path, times)
260         else:
261             return new_path
262 
263 
264     def cmd_get(self,*args):
265         '''
266         download the file specified
267         :param args[1]: instructions
268         :return:
269         进度条,断点续传,配额,完整性验证
270         '''
271 
272         self.socket.send(args[1].encode())
273         filename = os.path.basename(args[1].split(maxsplit=1)[1] if len(args[1].split()) > 1 else '')
274 
275         exist_flag = self.socket.recv(1024)
276         if exist_flag == b'0':
277             file_path = os.path.join(download_path,filename)
278             if os.path.isfile(file_path):
279                 file_path = self.__rename_file(file_path)
280 
281             temp_file_path = ''.join([file_path,'.downloading'])
282 
283             get_progress = os.stat(temp_file_path).st_size if os.path.isfile(temp_file_path) else 0
284             # print('get_progress: ',get_progress)
285             self.socket.send(str(get_progress).encode())
286 
287             filesize = int(self.socket.recv(1024).decode('utf-8'))
288 
289             last_sent_file_size = get_progress
290             print('Getting \'%s\': '%filename,end='')
291             start_time = time.time()
292 
293             m = hashlib.md5()
294             try:
295                 with open(temp_file_path, 'a+b') as f:
296                     f.seek(0,0)
297                     m.update(f.read())
298                     print('#'*(get_progress*100//filesize),end='')
299 
300                     while get_progress < filesize:
301                         if get_progress >= filesize -8192:
302                             buffersize = filesize - get_progress
303                         else:
304                             buffersize = 8192
305 
306                         accepting_data = self.socket.recv(buffersize)
307 
308                         m.update(accepting_data)
309                         get_progress += len(accepting_data)
310                         f.write(accepting_data)
311 
312                         # 打印进度条
313                         num_bar_unit = (get_progress-last_sent_file_size)*100//filesize
314                         if num_bar_unit:
315                             sys.stdout.write('#'*num_bar_unit)
316                             sys.stdout.flush()
317                             last_sent_file_size = get_progress
318 
319             except Exception as e:
320                 print(e)
321                 print('Transfer interrupted')
322 
323                 self.socket.close()
324             else:
325                 os.rename(''.join([file_path,'.downloading']),file_path)
326 
327                 end_time = time.time()
328                 download_speed = filesize/1024/(end_time-start_time)
329                 # print(1)
330                 md5_from_server = self.socket.recv(1024).decode('utf-8')
331                 # print(2)
332                 print('\nTransfer completed.\nFile integrity authentication %s\nUpload speed:%.1d k/s'
333                           %('succeeded' if md5_from_server == m.hexdigest() else 'failed',download_speed))
334 
335         else:
336             print('File \'\033[1;31m%s\033[0m\' does not exists'%filename)
337 
338 
339     def cmd_exit(self,*args):
340         self.socket.send(b'exit')
341         self.break_flag = 1
342         self.socket.close()
343 
344 
345     def interactive(self):
346         '''handle all interactive actions here'''
347 
348         try:
349             self.break_flag = 0
350 
351             self.authenticate()
352 
353             while 1:
354                 instructions = input('>> ').strip()
355 
356                 if instructions == '':
357                     print('Input can not be empty!')
358                     continue
359 
360                 cmd = instructions.split(maxsplit=1)[0]
361 
362                 if hasattr(self,''.join(['cmd_',cmd])):
363                     func = getattr(self,''.join(['cmd_',cmd]))
364                     func(self,instructions)
365 
366                     if self.break_flag:
367                         break
368 
369                 else:
370                     print('\033[1;31m%s:command not found\033[0m'%cmd)
371                     self.help()
372 
373         except ConnectionResetError as e:
374             print(e)
375             print('Connection has interrupted')
376         except Exception as e:
377             print(e)
378             print('\033[1;31mUnknown error,please check if your program operates properly\033[0m')
379 
380 def main():
381     client = FtpClient()  # 创建套接字
382 
383     server_ip = setting.server_addr['ip']
384     server_port = setting.server_addr['port']
385 
386     client.connect(server_ip,server_port)  # 连接服务器
387 
388     client.interactive()  # 与服务器交互
389 
390 
391 if __name__ == '__main__':
392     main()
main.py

 

posted @ 2017-07-13 10:46  jailly  阅读(497)  评论(0编辑  收藏  举报