Python实现不压缩带有文件夹文件传输工具

前言

需求背景

  1. 不管是微信,QQ发送文件还是ftp工具,目前尚未发现可以直接传输带有文件夹的文件(可能存在我未发现),一般都是将该文件压缩后传输。

    微信不支持发送文件夹截图:
    在这里插入图片描述

  2. 不知道为什么使用ftp工具从阿里云上传输文件到本地上巨慢。

  3. 发送方发送文件想让多个接收方可以同时接收。

脚本功能

  1. 支持发送文件夹内容,速度非常快(但是也要看网络状况),在本地测试将一个文件夹文件发送到另一个目录,400多M只需要三秒。当然如果连接的是远程主机速度就没这么快了。
  2. 服务端发送文件是多线程的,支持多个客户端同时接收发送文件夹文件。

使用介绍

程序目录结构:
在这里插入图片描述

发送方
在这里插入图片描述

在该主机上运行server2.py文件:

需要注意的是参数一定是发送文件夹的绝对路径。
在这里插入图片描述

当接收方连接后打印接收方套接字:
在这里插入图片描述

接收方:

在接收方运行client2.py文件:
在这里插入图片描述

传输成功

远程主机639M左右发送了261秒左右, 视网络状况而定。
在这里插入图片描述

查看下本地是否已经接收

接收的文件都是经过MD5校验的,所以一旦接收成功,可以保证文件的正确性。
在这里插入图片描述

脚本代码

server2.py:

如果需要修改发送方套接字改这行代码可以了sock_listen.bind(("0.0.0.0", 9996))

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import socket
import sys
import os
import threading
import hashlib
'''
文件夹传输服务器端程序
传输协议:
1. 基于TCP协议;
2. 客户端连接服务器成功后,客户端不发送任何消息,服务器端首先将文件的描述信息
    (定长包头,长度为347B)发送给客户端,紧接着发送文件数据给客户端,
    发送文件数据后断开连接;
3. 文件描述信息结构为:文件相对发送文件夹的相对路径名(300B,右边填充空格,UTF-8编码)
    +文件大小(15B,右边填充空格)+ 文件MD5值(32B, 大写形式)
4. 若文件夹中有空文件夹,发送空文件夹时,文件描述信息为:空文件夹相对发送文件夹的相对路径名
    (300B,右边填充空格,UTF-8编码)
    +文件大小设为-1(15B,右边填充空格)+ 文件MD5值设为(' ' *32),即32个空格(32B, 大写形式)
'''

def get_file_md5(file_path):
    '''
    函数功能:生成发送文件的MD5
    参数描述:
        file_path 发送文件的路径

    '''
    m = hashlib.md5()

    with open(file_path, "rb") as f:
        while True:
            data = f.read(1024)
            if len(data) == 0:
                break    
            m.update(data)
    
    return m.hexdigest().upper()

def send_empty_dir(sock_conn, file_abs_path, dest_file_parent_path):
    '''
    函数功能:将目标文件夹中所含的空文件夹发送给客户端
    参数描述:
        sock_conn 套接字对象
        dir_abs_path 空文件夹文件的绝对路径
        dest_file_parent_path 待发送文件的文件目录的相对路径(相对于发送的文件夹)
    '''
    file_name = file_abs_path[len(dest_file_parent_path):] # 获取目标文件夹的名字
    # 切完后的file_name可能是这样的“\a”,在Linux下“/a”,去掉斜杠
    if file_name[0] == '\\' or file_name[0] == '/':
        file_name = file_name[1:]
    # 将空文件夹下的文件的大小设为-1,MD5设为32个空格    
    file_size = -1
    file_md5 = ' ' * 32
    # 根据传输协议,设置包头内容
    file_name = file_name.encode()
    file_name += b' ' * (300 - len(file_name))
    file_size = "{:<15}".format(file_size).encode()

    file_desc_info = file_name + file_size + file_md5.encode() #加码后的包头内容
    # 发送空文件夹的包头
    sock_conn.send(file_desc_info)
    
def send_one_file(sock_conn, file_abs_path, dest_file_parent_path):
    '''
    函数功能:将一个文件发送给客户端
    参数描述:
        sock_conn 套接字对象
        file_abs_path 待发送的文件的绝对路径
        dest_file_parent_path 待发送文件的文件目录的相对路径(相对于发送的文件夹)
    '''
    file_name = file_abs_path[len(dest_file_parent_path):] # 获取目标文件夹的名字
    # 切完后的file_name可能是这样的“\a”,在Linux下“/a”,去掉斜杠
    if file_name[0] == '\\' or file_name[0] == '/':
        file_name = file_name[1:]

    file_size = os.path.getsize(file_abs_path)
    file_md5 = get_file_md5(file_abs_path)
    # 将空文件夹下的文件的大小设为-1,MD5设为32个空格
    file_name = file_name.encode()
    file_name += b' ' * (300 - len(file_name))
    file_size = "{:<15}".format(file_size).encode()

    file_desc_info = file_name + file_size + file_md5.encode() #加码后的包头内容
    # 发送目标文件夹下发送文件的包头
    sock_conn.send(file_desc_info)
    # 变读取文件内容变发送
    with open(file_abs_path, "rb") as f:
        while True:
            data = f.read(1024)
            if len(data) == 0:
                break
            sock_conn.send(data)


def send_file_thread(sock_conn, dest_file_abs_path,dest_file_parent_path):
    '''
    函数功能:遍历目标文件夹下的子文件夹、以及所有文件,调用发送文件函数和发送空文件夹函数
    参数描述:sock_conn 连接套接字
             dest_file_abs_path 目标文件夹的绝对路径
             dest_file_parent_path 目标文件夹父目录的绝对路径
    '''
    try:
        for root, dirs, files in os.walk(dest_file_abs_path):
            # 如果是空文件夹,就掉用空文件夹发送函数
            if len(dirs) == 0 and len(files) ==0:
                send_empty_dir(sock_conn, root, dest_file_parent_path)
            # 如果有文件,就调用发送文件的函数
            for f in files:
                file_abs_path = os.path.join(root, f) # 获取文件的绝对路径
                print(file_abs_path)
                send_one_file(sock_conn, file_abs_path,dest_file_parent_path)
    except Exception as e:
        print(e)
    finally:
        sock_conn.close()


def main ():


    dest_file_abs_path = os.path.abspath(sys.argv[1]) # 获取当前用户输入(在终端中打开)的文件夹的绝对路径
    dest_file_parent_path = os.path.dirname(dest_file_abs_path) #获取目标文件夹的父目录的绝对路径

    # 绑定套接字,监听
    sock_listen = socket.socket()
    sock_listen.bind(("0.0.0.0", 9996))
    sock_listen.listen(5)

    while True:
        sock_conn, client_addr = sock_listen.accept()
        print(client_addr, "已连接!")
        # 调用发送的线程
        threading.Thread(target=send_file_thread, args=(sock_conn, dest_file_abs_path, dest_file_parent_path)).start()

    sock_listen.close()

if __name__ == "__main__":
    main()

client2.py

修改接收路径改这行代码os.chdir(r"C:\Download")

import socket
import os
import hashlib
import time

'''
文件夹传输客户端程序
传输协议:
1. 基于TCP协议;
2. 客户端连接服务器成功后,客户端不发送任何消息,服务器端首先将文件的描述信息
    (定长包头,长度为347B)发送给客户端,紧接着发送文件数据给客户端,
    发送文件数据后断开连接;
3. 文件描述信息结构为:文件相对发送文件夹的相对路径名(300B,右边填充空格,UTF-8编码)
    +文件大小(15B,右边填充空格)+ 文件MD5值(32B, 大写形式)
4. 若文件夹中有空文件夹,发送空文件夹时,文件描述信息为:空文件夹相对发送文件夹的相对路径名
    (300B,右边填充空格,UTF-8编码)
    +文件大小设为-1(15B,右边填充空格)+ 文件MD5值设为(' ' *32),即32个空格(32B, 大写形式)
'''

def get_file_md5(file_name_path):
    '''
    函数功能:将发送过来的文件生成MD5值
    参数描述:
        file_name_path 接收文件的绝对路径

    '''
    m = hashlib.md5()
    #print(file_name_path)
    with open(file_name_path, "rb") as f:
        while True:
            data = f.read(1024)
            if len(data) == 0:
                break    
            m.update(data)
    
    return m.hexdigest().upper()


def recv_file(sock):
    '''
    函数功能:根据协议循环接收包头,根据接收到的包头(文件描述信息结构)创建文件夹及其子文件夹,
             在相应目录下写入文件
    参数描述:
        sock 连接套接字

    '''
    while True:
        # 根据协议先接收文件名或空文件夹名的相对路径的字节
        file_name_path = sock.recv(300).decode().rstrip()
        print(file_name_path)
        # 若文件名长度为0,说明接收文件夹完毕,跳出循环
        if len(file_name_path) == 0 : 
            break
        # 接收文件大小的字节
        file_size = sock.recv(15).decode().rstrip()
        # 若文件大小字节为'-1',根据协议接收到的为空文件夹
        if int(file_size) == -1:
            print("成功接收空文件夹!{}".format(file_name_path))
            os.makedirs(file_name_path,exist_ok= True)
            continue # 继续循环
        # 若接收到的文件大小字节的长度为0,说明接收文件夹完毕,跳出循环
        if len(file_size) == 0:
            break
        #print(file_size)
        # 接收MD5字节,若长度为0,跳出循环
        file_md5 = sock.recv(32).decode().rstrip()
        if len(file_md5) == 0:
            break
        #print(file_md5)
        # 根据接收到的包头信息,创建文件夹
        os.makedirs(os.path.dirname(file_name_path), exist_ok=True)

        file_name = os.path.basename(file_name_path)
        # 根据文件大小循环接收并写入
        recv_size = 0
        start_time = time.time() 

        f = open(file_name_path, "wb")                                            #初始化已接收到的字节
        while recv_size < int(file_size):                             #若接收到的字节小于文件大小就循环接收,并写入本地
            file_data = sock.recv(int(file_size) - recv_size)
            if len(file_data) == 0:
                break

            f.write(file_data)            

            recv_size += len(file_data)
            print("\r正在接收{}文件,已接收{}字节".format(file_name, recv_size),end= ' ')
        f.close()

        end_time = time.time()
        t = end_time - start_time
        # 调用MD5生成函数检验收到的文件是否是发送方的原文件
        recv_file_md5 = get_file_md5(file_name_path)          
        if recv_file_md5 == file_md5:
            print("\n成功接收文件{},用时{:.2f}s\n".format(file_name, t))
        else:
            print("接收文件 %s 失败(MD5校验不通过)\n" % file_name)
            break


def main():
    server_ip = input("请输入目标主机地址:")
    server_port = int(input("请输入目标主机端口:"))
    sock_bind = (server_ip, server_port)
    sock = socket.socket()
    sock.connect(sock_bind)
    os.chdir(r"C:\Download")        # 先进入所要接收文件夹的目录
    recv_file(sock)
    print("文件夹接收成功!")
    sock.close()

if __name__ == "__main__":
    main()
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一切如来心秘密

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值