def re_put(self, select_file, file_put_unfinished_list): """ 断点续传 :param select_file: :param file_put_unfinished_list: :return: """ file_total_size = self.shelve_obj[select_file][1] uuid_file_name = self.shelve_obj[select_file][2] file_server_abs_path = self.shelve_obj[select_file][3] source_file_md5_value = self.shelve_obj[select_file][4] # 移除出去,在退出时不需要在服务端删除该文件了 file_put_unfinished_list.remove(file_server_abs_path) self.send_certain_size_msg("re_put", file_total_size=file_total_size, uuid_file_name=uuid_file_name, file_server_abs_path=file_server_abs_path, source_file_md5_value=source_file_md5_value) response_data = self.get_response_from_server() print_info(response_data["response_msg"]) f = open(select_file, "rb") finished_file_size = response_data["received_file_size"] f.seek(finished_file_size) # 发送文件中的数据到服务端 self.send_data_from_file(f, finished_file_size, file_total_size) # 上传完成,删除断点续传的文件中的信息 del self.shelve_obj[select_file]
def get(self, client_request_cmd, request_conn_obj): """ 从服务端下载文件 :param client_request_cmd: :param request_conn_obj: :return: """ current_thread_name = currentThread().name user_current_dir = self.user_info[current_thread_name]["user_current_dir"] # 客户端上传的相对路径get a/b/test.txt,相对路径与用户当前所在路径想拼 file_relative_path = client_request_cmd["file_relative_path"] # 绝对路径 file_abs_path = os.path.join(user_current_dir, file_relative_path) if os.path.exists(file_abs_path): file_total_size = os.path.getsize(file_abs_path) # 获取源文件的md5值 file_md5_value = self.file_md5_value(file_abs_path) self.send_certain_size_response("200", request_conn_obj, file_total_size=file_total_size, file_md5_value=file_md5_value) f = open(file_abs_path, "rb") for line in f: request_conn_obj.send(line) f.close() print_info("file transfer successful!") else: print_info(self.STATUS_CODE["201"]) self.send_certain_size_response("201", request_conn_obj)
def create_user(self, client_request_cmd, request_conn_obj): """ 创建新用户,验证用户是否存在,同时在home目录下创建文件夹 :param client_request_cmd: :param request_conn_obj: :return: """ new_user_name = client_request_cmd["new_user_name"] new_user_password = client_request_cmd["new_user_password"] # 用户输入的是G,保存到文件中是B,字节,便于后面比较大小 # 要转换为str才能保存到ini文件中,由于用户可能输入小数,所以用float user_quota = str(float(client_request_cmd["user_quota"].replace("G", ""))*1024*1024*1024) # 保存字节到文件中 # 加密后的密码 new_user_password = self.hash_password(new_user_password) # 新加的用户,在用户家目录下创建目录 new_user_home_dir = os.path.join(settings.USER_HOME_DIR, new_user_name) if self.accounts.has_section(new_user_name): print_info(self.STATUS_CODE["501"]) self.send_certain_size_response("501", request_conn_obj) else: self.accounts.add_section(new_user_name) self.accounts.set(new_user_name, "password", new_user_password) self.accounts.set(new_user_name, "quota", user_quota) self.accounts.set(new_user_name, "left_quota", user_quota) self.accounts.write((open(settings.ACCOUNT_FILE, "w"))) self.send_certain_size_response("500", request_conn_obj) # 创建用户家目录 os.mkdir(new_user_home_dir) print_info(self.STATUS_CODE["500"])
def __print_help_msg(): """ 打印启动的帮助信息,只是内部使用,不对外使用,可以设置为隐藏函数 :return: """ help_msg = """ [python run_server.py start] to start 7.ftp server! [python run_server.py stop] to stop 7.ftp server! """ print_info(help_msg)
def verify_file_md5(self, source_file_md5_value, finally_file_storage_path): """ 验证文件的md5 :param source_file_md5_value: :param finally_file_storage_path: :return: """ file_md5_value = self.file_md5_value(finally_file_storage_path) if source_file_md5_value == file_md5_value: print_info("\n file is same with sever,file download successful!") else: print_info("\n file is not same with server!", "error")
def verify_md5(self, source_file_md5, file_abs_path): """ 用于验证接受的文件,与客户端的文件的md5是否相同 :param source_file_md5: :param file_abs_path: :return: """ file_md5_value = self.file_md5_value(file_abs_path) # 验证文件是否和客户端一样 if file_md5_value == source_file_md5: print_info("file is same with client,file received done") else: print_info("file is not same with client!", "error")
def quit(self, client_cmd_list): """ 退出客户端,用户只能输入quit,所以len(cmd_list)==0 :return: """ if self.verify_client_cmd_list(client_cmd_list, 0): self.send_certain_size_msg("quit") print_info("%s waiting for you come again! bye bye!" % self.username) exit() else: print_info("your input is illegal!you can just input [quit]!", "error")
def put(self, client_cmd_list): """ 这里就只允许用户上传所在系统的任意文件,但是需要写出绝对路径 :param client_cmd_list: :return: """ if self.verify_client_cmd_list(client_cmd_list, 1): file_abs_path = client_cmd_list[0] if os.path.isfile(file_abs_path): file_total_size = os.path.getsize(file_abs_path) # 先把文件的信息发送到服务端,然后在发送文件的内容 if self.left_quota > file_total_size: # 获取md5值,用于验证数据的准确性 file_md5_value = self.file_md5_value(file_abs_path) file_name = os.path.basename(file_abs_path) # 把文件名发送到服务端,服务端验证文件是否已经存在,如果已经存在,就重命名为uuid-filename self.send_certain_size_msg("put", file_total_size=file_total_size, file_name=file_name, file_md5_value=file_md5_value) # 获取服务端发送的结果 response_data = self.get_response_from_server() print_info(response_data["response_msg"]) # 如果上传的文件已经存在,服务端会把文件重命名为uuid-file_name,这时只能从服务端获取到该文件名 # 用于断点续传使用 uuid_file_name = response_data["file_name"] # 文件在服务端的绝对路径 file_server_abs_path = response_data["file_abs_path"] self.shelve_obj[file_abs_path] = [ "put_unfinished", file_total_size, uuid_file_name, file_server_abs_path, file_md5_value ] # 用于断点续传的信息存储 f = open(file_abs_path, "rb") finished_file_size = 0 self.send_data_from_file(f, finished_file_size, file_total_size) # 上传完成,删除断点续传的文件中的信息 del self.shelve_obj[file_abs_path] print_info("\nfile transfer finished!") else: print_info("left quota is not enough!") else: print_info("your put file is not a file!") else: print_info("your input is illegal!for example [put e:\\file.txt]")
def login(self): """ 用户登录方法 :return: """ if self.auth(): self.unfinished_file_deal() while True: # 用户输入ls, get file ,put file,cd dir print_info("you can input [quit] to exit client!") client_cmd = input("your input %s#" % self.terminal_display).strip() if not client_cmd: continue client_cmd_list = client_cmd.split() # 用户输入的第一个命令 if hasattr(self, client_cmd_list[0]): func = getattr(self, client_cmd_list[0]) # 把后面的内容传给方法 func(client_cmd_list[1:])
def re_get(self, client_request_cmd, request_conn_obj): """ 重新获取没有获取完成的数据 :param client_request_cmd: :param request_conn_obj: :return: """ response_data = client_request_cmd file_abs_path = response_data["file_abs_path"] file_total_size = response_data["file_total_size"] if os.path.exists(file_abs_path): if os.path.getsize(file_abs_path) == file_total_size: self.send_certain_size_response("600", request_conn_obj) received_file_size = response_data["received_file_size"] f = open(file_abs_path, "rb") f.seek(received_file_size) for line in f: request_conn_obj.send(line) f.close() print_info("%s file re_get success" % file_abs_path) else: # source file changed,you can not re_get print_info(self.STATUS_CODE["602"]) self.send_certain_size_response("602", request_conn_obj) else: # re_get file not exist print_info(self.STATUS_CODE["601"]) self.send_certain_size_response("601", request_conn_obj)
def create_user(self): """ 新建用户,同时在服务端Home目录下创建家目录,并设置用户磁盘空间大小 :return: """ exit_flag = True while exit_flag: new_user_name = input("please input new user name:").strip() new_user_password = input("please set user password:"******"please set user quota[G]:").strip() # 验证用户的输入是否标准 # 其中用户配额,由于单位过多,所以这里只能存G,服务端会进行转换为字节 # 这里会验证用户配额输入的否标准 if new_user_name and new_user_password and user_quota.endswith("G") \ and user_quota.replace("G", "").replace(".", "").isdigit(): self.send_certain_size_msg("create_user", new_user_name=new_user_name, new_user_password=new_user_password, user_quota=user_quota) response_data = self.get_response_from_server() if response_data["response_code"] == "500": print_info(response_data["response_msg"]) exit_flag = False else: print_info(response_data["response_msg"]) exit_flag = False else: print_info("your input is illegal!") exit_flag = False
def auth(self): """ 用户登录方法,首先是用户登录,登录成功后才能进行后面的操作 :return: """ count = 0 while count < 3: username = input("please input username:"******"please input password:"******"auth", username=username, password=password) response_data = self.get_response_from_server() if response_data["response_code"] == "100": print_info(response_data["response_msg"]) self.terminal_display = "[%s]" % username self.username = username self.left_quota = response_data["left_quota"] # 用于断点续传处的路径拼接 self.user_current_dir = response_data["user_current_dir"] return True else: print_info(response_data["response_msg"], "error") count += 1 else: print_info("username or password an not be null!", "error") count += 1 return False
def cd(self, client_cmd_list): """ cd a/b 由于用户只能在自己的家目录下下切换,所以会把用户输入的目录与服务端的user_current_dir拼 :param client_cmd_list: :return: """ if self.verify_client_cmd_list(client_cmd_list, 1): target_path = client_cmd_list[0] self.send_certain_size_msg("cd", target_path=target_path) response_data = self.get_response_from_server() if response_data["response_code"] == "400": client_terminal_display_dir = response_data[ "client_terminal_display_dir"] if len(client_terminal_display_dir) == 0: self.terminal_display = "[%s]" % self.username else: self.terminal_display = "[%s]" % client_terminal_display_dir # 记录用户的当前路径,用于断点续传时,获取文件使用 self.user_current_dir = response_data["user_current_dir"] print_info("change dir success!") else: print_info(response_data["response_msg"]) else: print_info("your input is illegal!for example [cd a/b ]")
def ls(self, client_cmd_list): """ 列出用户当前目录下的内容,当前目录是以服务端的self.user_current_dir为准 :param client_cmd_list: :return: """ if self.verify_client_cmd_list(client_cmd_list, 0): self.send_certain_size_msg("ls") response_data = self.get_response_from_server() if response_data["response_code"] == "300": print_info(response_data["response_msg"]) cmd_result_total_size = response_data["cmd_result_total_size"] received_file_size = 0 while received_file_size < cmd_result_total_size: left_file_size = cmd_result_total_size - received_file_size if left_file_size < self.MSG_SIZE: data = self.client_socket_obj.recv(left_file_size) # 这样操作是不可以的len(data)!=left_file_size # received_file_size += left_file_size else: data = self.client_socket_obj.recv(self.MSG_SIZE) # received_file_size += self.MSG_SIZE received_file_size += len(data) print_info(data.decode("gbk")) else: print_info("your input is illegal!for example [ls ]")
def send_certain_size_msg(self, action_type, **kwargs): """ 发送固定打下的包到服务端,这是处理粘包问题的必备思路 首先发送一个固定大小的包到对方,对方会按照指定大小接受,如果是文件,数据包中会含有要发送的文件的大小 后面再发送文件或其他内容,对方只需要知道文件的大小,通过循环,即可接受全部的内容 :param action_type: :param kwargs: :return: """ msg_data = {"action_type": action_type, "fill": ""} # 更新msg_data字典,因为可能会加入新的key,value放入到了kwargs中 msg_data.update(kwargs) bytes_msg_data = json.dumps(msg_data).encode("utf-8") # 为了避免粘包问题,每次首先都发送一个定长的数据(这里定长为1024字节,足够使用了)到服务端, # 服务端根据这个数据中的信息进行后面的操作 if len(bytes_msg_data) < self.MSG_SIZE: # 进行数据填充,使得长度为1024字节 msg_data["fill"] = msg_data["fill"].zfill(self.MSG_SIZE - len(bytes_msg_data)) bytes_msg_data = json.dumps(msg_data).encode("utf-8") self.client_socket_obj.send(bytes_msg_data) print_info("your commands has send to 7.ftp server successful!")
def cd(self, client_request_cmd, request_conn_obj): """ 切换目录 :param client_request_cmd: :param request_conn_obj: :return: """ current_thread_name = currentThread().name user_current_dir = self.user_info[current_thread_name]["user_current_dir"] user_home_dir = self.user_info[current_thread_name]["user_home_dir"] print("------", self.user_info) target_path = client_request_cmd["target_path"] # E:\PythonProject\python-test\homework\7.ftp\server\home\vita\test\..\..使用abspath能去掉.. # 变为E:\PythonProject\python-test\homework\7.ftp\server\home\vita\test target_abs_path = os.path.abspath(os.path.join(user_current_dir, target_path)) print_info(target_abs_path) if os.path.isdir(target_abs_path): if target_abs_path.startswith(user_home_dir): client_terminal_display_dir = target_abs_path.replace(user_home_dir, "") print_info(self.STATUS_CODE["400"]) user_current_dir = target_abs_path self.user_info[current_thread_name]["user_current_dir"] = target_abs_path self.send_certain_size_response("400", request_conn_obj, client_terminal_display_dir=client_terminal_display_dir, user_current_dir=user_current_dir) print_info("dir change success!") else: # 输出无权限 self.send_certain_size_response("402", request_conn_obj) print_info(self.STATUS_CODE["402"]) else: # 输出目录不存在 self.send_certain_size_response("401", request_conn_obj) print_info(self.STATUS_CODE["401"])
def interactive(self): """ 与用户交互的方法 :return: """ exit_flag = True while exit_flag: info = """ 1.创建用户 2.登录 3.退出 """ choice_list = {"1": "create_user", "2": "1.login", "3": "quit"} print_info(info) your_choice = input("please input your choice:").strip() if your_choice in choice_list: if choice_list[your_choice] == "quit": self.quit([]) else: func = getattr(self, choice_list[your_choice]) func() else: print_info("your input is illegal")
def keep_running(self): """ 这里是允许多用户登录,但这里没有使用多线程,是通过while True的方式, 一个用户断开连接,另一个用户才能继续连接 :return: """ self.server_socket_obj.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server_socket_obj.bind((settings.FTP_SERVER_HOST, settings.FTP_SERVER_PORT)) self.server_socket_obj.listen(5) while True: print("waiting client to connect!") request_conn_obj, client_addr = self.server_socket_obj.accept() # self.client_addr是个元组('127.0.0.1','13850') print_info("client connect:ip:%s port:%s" % (client_addr[0], client_addr[1])) try: t = self.pool.get_thread() # 每次创建都从队列中拿取一个线程对象 obj = t(target=self.handle_client_request, args=(request_conn_obj,)) # 使用线程对象创建线程 obj.start() # 启动线程 # self.handle_client_request(request_conn_obj) # 客户端输入quit退出程序时,服务端的 except Exception as e: print(e) print_info("client closed connection!ip:%s port:%s" % (client_addr[0], client_addr[1]))
def get(self, client_cmd_list): """ 从服务端下载文件,保存到客户端的file_storage路径下,包含md5验证 :param client_cmd_list: :return: """ if self.verify_client_cmd_list(client_cmd_list, 1): # get a/b/test.txt会从服务端的当前所在路径下的a/b/中获取test.txt file_relative_path = client_cmd_list[0] file_name = os.path.basename(file_relative_path) self.send_certain_size_msg("get", file_relative_path=file_relative_path) response_data = self.get_response_from_server() if response_data["response_code"] == "200": # 判断要下载的文件在本地下载路径中是否存在,如果已经存在,就重命名 if os.path.exists( os.path.join(settings.FILE_STORAGE_PATH, file_name)): # print("ewe-wew".split("-")[1])这样就可获取到原文件名,便于后面操作, # 因为重名后,只有客户端有这文件 file_name = "%s-%s" % (uuid.uuid1(), file_name) # 存放文件的路径,放在storage下,先存为.download,用于断点续传 file_storage_path_download = os.path.join( settings.FILE_STORAGE_PATH, "%s.download" % file_name) print_info(response_data["response_msg"]) file_total_size = response_data["file_total_size"] source_file_md5_value = response_data["file_md5_value"] # 准备开始写入文件 f = open(file_storage_path_download, "wb") # 把下载的文件信息保存到shelve文件中,供断点续传的信息记录 source_server_file_abs_path = os.path.join( self.user_current_dir, file_relative_path) self.shelve_obj[source_server_file_abs_path] = [ "get_unfinished", file_total_size, "%s.download" % file_name, source_file_md5_value ] # 用于断点续传信息记录完成 received_file_size = 0 self.write_data_from_receive(f, received_file_size, file_total_size) # 文件下载成功,重命名文件 finally_file_storage_path = os.path.join( settings.FILE_STORAGE_PATH, file_name) os.rename(file_storage_path_download, finally_file_storage_path) del self.shelve_obj[source_server_file_abs_path] # 文件下载成功,把用于断点续传消息记录的该文件消息删除 self.verify_file_md5(source_file_md5_value, finally_file_storage_path) else: print_info(response_data["response_msg"]) else: print_info("your input is illegal!for example [get file.txt]")
def auth(self, client_request_cmd, request_conn_obj): """ 用户登录验证 :param client_request_cmd: :param request_conn_obj: :return: """ current_thread_name = currentThread().name username = client_request_cmd["username"] password = client_request_cmd["password"] # 加密后的密码 password = self.hash_password(password) if username in self.accounts: real_password = self.accounts[username]["password"] if password == real_password: user_current_dir = os.path.join(settings.USER_HOME_DIR, username) user_home_dir = os.path.join(settings.USER_HOME_DIR, username) username = username # 返回给客户端,用于记录当前用户的磁盘配额,每次put, # 客户端直接从自己记录的配额中减去, # 验证时也直接使用自己的配额记录,就不需要再次从服务端获取了 # 服务端也会进行减法 left_quota = float(self.accounts[username]["left_quota"]) self.user_info[current_thread_name] = {} self.user_info[current_thread_name]["user_current_dir"] = user_current_dir self.user_info[current_thread_name]["user_home_dir"] = user_home_dir self.user_info[current_thread_name]["username"] = username self.user_info[current_thread_name]["left_quota"] = left_quota self.send_certain_size_response("100", request_conn_obj, left_quota=left_quota, user_current_dir=user_current_dir) print_info(self.STATUS_CODE["100"]) else: print_info("your password is not correct", "error") self.send_certain_size_response("101", request_conn_obj) else: print_info("user %s not exist" % username, "error") self.send_certain_size_response("102", request_conn_obj)
def unfinished_file_deal(self): """ 断点续传处理 :return: """ out_exit_flag = True while out_exit_flag: # 如果没有断点的文件,直接退出 if len(list(self.shelve_obj.keys())) == 0: break # 如果有没有上传成功的文件,同时用户不想处理,就自动在服务端把改文件删除,因为是无用文件, # 同时也是为了用户磁盘配额的准确性,因为在程序突然中断,包含服务端和客户端,还没有把剩余的配额写入文件中 file_put_unfinished_list = [] for index, file_abs_path in enumerate(self.shelve_obj.keys()): # self.shelve_obj[source_server_file_abs_path] = ["get_unfinished", # file_total_size, # "%s.download" % file_name] info = self.shelve_obj[file_abs_path][0] file_total_size = self.shelve_obj[file_abs_path][1] file_name = self.shelve_obj[file_abs_path][2] if info == "get_unfinished": received_file_size = os.path.getsize( os.path.join(settings.FILE_STORAGE_PATH, file_name)) received_percent = received_file_size / file_total_size * 100 print(index, info, file_name, "received_file_size:%s " % received_file_size, "file_total_size:%s" % file_total_size, "received_percent[百分比]:%s" % received_percent) elif info == "put_unfinished": # self.shelve_obj[file_abs_path] = ["put_unfinished", file_total_size, # uuid_file_name, file_server_abs_path] # 加到列表中,用于后面的处理 file_put_unfinished_list.append( self.shelve_obj[file_abs_path][3]) print(index, info, file_name) exit_flag = True while exit_flag: your_choice = input( "you can input [quit] to finish deal unfinished file!" "please input your choice:").strip() if your_choice == "quit": # 如果有没有上传成功的,就删除服务端已经上传的部分,无用的文件就删除掉吧 if len(file_put_unfinished_list) != 0: self.send_certain_size_msg( "del_unfinished_put", file_put_unfinished_list=file_put_unfinished_list) response_data = self.get_response_from_server() print_info(response_data["response_msg"]) # 用户选择不处理,就删除文件中的内容,不能每次登录都打扰用户 for index, file_abs_path in enumerate( self.shelve_obj.keys()): del self.shelve_obj[file_abs_path] out_exit_flag = False break keys_list = list(self.shelve_obj.keys()) if your_choice.isdigit( ) and int(your_choice) <= len(keys_list): your_choice = int(your_choice) select_file = keys_list[your_choice] # self.shelve_obj[source_server_file_abs_path] = ["get_unfinished", # file_total_size, # "%s.download" % file_name, # source_file_md5_value] if self.shelve_obj[select_file][0] == "get_unfinished": file_total_size = self.shelve_obj[select_file][1] file_name = self.shelve_obj[select_file][2] source_file_md5_value = self.shelve_obj[select_file][3] file_storage_path = os.path.join( settings.FILE_STORAGE_PATH, file_name) received_file_size = os.path.getsize(file_storage_path) self.send_certain_size_msg( "re_get", file_abs_path=select_file, file_total_size=file_total_size, file_name=file_name, received_file_size=received_file_size) response_data = self.get_response_from_server() if response_data["response_code"] == "600": f = open(file_storage_path, "ab") self.write_data_from_receive( f, received_file_size, file_total_size) finally_file_storage_path = file_storage_path.replace( ".download", "") os.rename(file_storage_path, finally_file_storage_path) # 重新获取文件,验证md5 self.verify_file_md5(source_file_md5_value, finally_file_storage_path) del self.shelve_obj[select_file] # out_exit_flag = False break else: print_info(response_data["response_msg"]) exit_flag = False # self.shelve_obj[file_abs_path] = ["put_unfinished", file_total_size, # uuid_file_name, file_server_abs_path, file_md5_value] elif self.shelve_obj[select_file][0] == "put_unfinished": # 继续未完成的继续上传 self.re_put(select_file, file_put_unfinished_list) print_info("\nfile re_put transfer finished!") break else: # 输入不是数字,提示输入时不合法的。 print("your input is illegal!")