文件的上传和下载

文件的上传和下载本质上都是两台计算机,建立连接,传输数据,然后把数据以文件的形式保存在硬盘中。

不同的协议会有不同的传输方式。例如 http 和 ftp 的传输方式肯定不一样,但本质上依然是传输数据,然后数据的接收方把数据保存成文件。

上传和下载是相对而言的,数据的发送方就是在上传,数据的接收方就是在下载。

HTTP 中的文件下载

前端上传文件的总结

http协议中关于文件上传的部分

RFC 7578

上传单个文件

关键是要把 input 的 type 设为 file

<form id="upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png"/></label>
    <button type="submit">Submit</button>
</form>

上传多个文件

关键是要在上一个例子中 input 标签里加上 multiple 属性

<form id="upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png" multiple/></label>
    <button type="submit">Submit</button>
</form>

用 ajax 上传

<form id="upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png" multiple/></label>
    <button type="submit">Submit</button>
</form>
<script>
document.querySelector('#split_upload_form').addEventListener('submit', function(event) {
    var form_data = new FormData(this);
    var xhr = new XMLHttpRequest();
    xhr.setRequestHeader('Content-type', 'multipart/form-data');
    xhr.open("POST", url, true);
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
            }
        }
    }
    xhr.send(form_data);
    event.cancelBubble = true;
    event.stopPropagation();
    event.preventDefault();
    return false;
}
</script>

分片上传

<form id="split_upload_form" action="" method="post" enctype="multipart/form-data">
    <label>label <input type="file" id="split_upload" name="split_upload" accept=".jpg, .jpeg, .png"/></label>
    <button type="submit">Submit</button>
</form>
<script>
(function(){
    class SplitUpload {
        element = {};
        maxSize = 1;
        acceptType = [];
        splitSize = 262144; // 一个分片的大小 262144B 256KB 0.25MB
        splitTimeout = 0; // 上传一个分片的最大时间
        totalTimeout = 0; // 上传全部分片的最大时间
        customData = {};
        shaType = 'SHA-256';
        url = '';
        constructor(options) {
            this.element = options.element;
            this.maxSize = options.maxSize;
            this.acceptType = options.acceptType;
            this.customData = options.customData ?? {};
            console.log(options);
        }
        async handle(files) {
            let shaType = this.shaType;
            for (let i = 0, len = files.length; i < len; i++) {
                let file = files[i];
                console.log(file);
                let fileSize = file.size;
                let fileType = file.type;
                let fileName = file.name;
                if (this.checkSize(fileSize)) {

                }
                if (this.checkSize(fileName, fileType)) {

                }
                let splitNum = 1;
                let splitSize = this.splitSize;
                if (fileSize > splitSize) {
                    splitNum = Math.ceil(fileSize / splitSize); // 需要读取的次数
                    console.log(splitNum);
                }

                let originalShaValue = await this.digest(shaType, file);
                for (let i = 0; i < splitNum; i++) {
                    // 计算分片起始位置
                    let start = i * splitSize;
                    // 计算分片结束位置
                    let end = start + splitSize;
                    // 最后一片特殊处理
                    if (end > fileSize) {
                        end = fileSize;
                    }
                    let file_temp_name = (new Date()).getTime() + "_" + parseInt(Math.random()*(999-100+1)+100,10);
                    let newBlob = file.slice(start, end);
                    let splitShaValue = await this.digest(shaType, newBlob);
                    let data = {
                        file_temp_name: file_temp_name,
                        splitNum: splitNum,
                        currentIndex: i,
                        file_real_name: fileName,
                        file_full_size: fileSize,
                        file_type: fileType,
                        blob: newBlob,
                        shaType: shaType,
                        splitShaValue: splitShaValue,
                        originalShaValue: originalShaValue,
                    };
                    data = Object.assign({}, data, this.customData);
                    this.sendAsync(this.url, data);
                }
            }
        }
        checkSize(size) {
            return true;
        }
        checkType(name, mime) {
            return true;
        }
        sendAsync(url, data) {
            var form_data = new FormData();
            for (let i in data) {
                form_data.append(i, data[i]);
            }
            return new Promise((resolve, reject) => {
                var xhr = new XMLHttpRequest();
                xhr.open("POST", url, true);
                // xhr.setRequestHeader('Content-type', 'multipart/form-dataPOST; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW');
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4) {
                        if (xhr.status == 200) {
                            if (xhr.responseText == "success") {
                                resolve("success");
                            } else {
                                reject("fail");
                            }
                        } else {
                            console.log(xhr)
                            reject("fail");
                        }
                    }
                }
                xhr.send(form_data);
            });
        }
        async digest(shaType, blob) {
            const blobArrayBuffer = await blob.arrayBuffer();                           // 编码为(utf-8)Uint8Array
            const hashBuffer = await crypto.subtle.digest(shaType, blobArrayBuffer);           // 计算消息的哈希值
            const hashArray = Array.from(new Uint8Array(hashBuffer));                     // 将缓冲区转换为字节数组
            const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // 将字节数组转换为十六进制字符串
            return new Promise((resolve, reject) => {
                resolve(hashHex);
            });
            // return hashHex;
        }
    };
    window.addEventListener('load', () => {
        let splitUpload = new SplitUpload({
                maxSize: 1,
                acceptType: [],
                splitSize: 1,
                splitTimeout: 0,
                totalTimeout: 0,
                customData: {},
            });
        document.querySelector('#split_upload_form').addEventListener('submit', function(event) {
            let fileInput = this.querySelector("input[type='file']");

            splitUpload.handle(fileInput.files);

            event.cancelBubble = true;
            event.stopPropagation();
            event.preventDefault();
            return false;
        })
    });
})();
</script>

用 php 处理分片上传的例子

function processShardedUploads() {
    $file_temp_name = $_POST['file_temp_name'] ?? null;
    $splitNum = $_POST['splitNum'] ?? null;
    $currentIndex = $_POST['currentIndex'] ?? null;
    $file_real_name = $_POST['file_real_name'] ?? null;
    $file_full_size = $_POST['file_full_size'] ?? null;
    $file_type = $_POST['file_type'] ?? null;
    $shaType = $_POST['shaType'] ?? null;
    $splitShaValue = $_POST['splitShaValue'] ?? null;
    $originalShaValue = $_POST['originalShaValue'] ?? null;
    $blob = $_FILES['blob'] ?? null;

    if (
        $file_temp_name === null ||
        $splitNum === null ||
        $currentIndex === null ||
        $file_real_name === null ||
        $file_full_size === null ||
        $file_type === null ||
        $blob === null ||
        $shaType === null ||
        $splitShaValue === null ||
        $originalShaValue === null
    ) {
        return 'missing parameter';
    }

    $checkHash = function($shaType, $tagValue, $filePath) {
        $shaType = strtolower(str_replace('-', '', $shaType));
        try {
            if (hash_file($shaType, $filePath) == $tagValue) {
                return true;
            }
        } catch (\Exception $e) {
        }
        return false;
    };
    // 验证分片的哈希值
    if (!$checkHash($shaType, $splitShaValue, $blob['tmp_name'])) {
        return 'fail split hash';
    }

    $uploadDir = dirname(__FILE__) . DIRECTORY_SEPARATOR; // 用于保存上传文件的目录
    $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'split_' . $file_real_name . '_' . $splitNum; // 用于保存分片的目录
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0777, true);
    }
    if (!is_dir($tempDir)) {
        mkdir($tempDir, 0777, true);
    }

    $clearTemp = function() use ($tempDir) {
        // 删除分片文件和文件夹
        array_map('unlink', glob($tempDir . DIRECTORY_SEPARATOR . '*'));
        rmdir($tempDir);
    };

    $baseFileName = $tempDir . DIRECTORY_SEPARATOR . $file_real_name;
    move_uploaded_file($blob['tmp_name'], $baseFileName . '_' . $currentIndex);
    sleep(mt_rand(1, 3)); // 随机暂停,防止上传速度过快导致无法合并文件

    if (count(glob($baseFileName . '_*')) != $splitNum) {
        return 'success split'; // 单个分片上传完,但还有其它分片没有上传完
    }

    // 合并文件
    $butffer = '';
    for ($i = 0; $i < $splitNum; $i++) {
        $sliceFileName = $baseFileName . '_' . $i;
        if (!is_file($sliceFileName)) {
            $clearTemp();
            return('fail merge ' . $i); // 合并的过程中缺失了某一个分片
        }
        $butffer .= file_get_contents($sliceFileName);
    }
    // 把合并后的文件复制到用于保存上传文件的目录
    $mergeFile = $uploadDir . DIRECTORY_SEPARATOR . $file_real_name;
    file_put_contents($mergeFile, $butffer);

    $clearTemp();

    // 验证合并后文件的哈希值
    if (!$checkHash($shaType, $originalShaValue, $mergeFile)) {
        unlink($mergeFile);
        return 'fail merge hash';
    }

    return 'success merge';
}

echo processShardedUploads();

上传时的进度条

断点下载

上传前的预览

参考

如何不使用浏览器在 windows 里下载文件

使用 Windows 自带的程序

下载文件方式的一些总结

各种协议

下载链接的前缀

thunder, Flashget, qqdl 随便百度一下就能找到解码的方法

各种软件

p2p

BT种子: 用一个文本文件(通常以 .torrent 作为后缀)描述一个或多个 Tracker 服务器或 DHT 网络中的文件。 ed2k: 用一段字符描述一个 KAD 网络中的文件。 Magnet: 用一段字符描述一个 DHT 网络中的BT种子文件。

其他

BitTorrent 既是一个下载协议也是一个软件名称。 大概就是作为软件的 BitTorrent 是最早使用 BitTorrent 协议下载的软件。

eDonkey 是一个商业软件, eMule 是 eDonkey 的开源版, VeryCD电驴 是国内的公司的根据 eMule 修改而来的。 kad 网络还有很多客户端,但在国内都不流行。

前互联网时代,好像都很喜欢在产品或软件或服务的名称前面加一个字母 e (例如 eMule eDonkey)

理论上 telnet 和 ssh 也可以用来下载文件。 理论上只要有网络连接就能下载文件,即使只能传输文本,也可以用 base64 来传输二进制数据。

参考