需求
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在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()
server_addr = { 'ip':'127.0.0.1', 'port':12345 }
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
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()
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()
1 server_addr = { 2 'ip':'127.0.0.1', 3 'port':12345 4 }
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()