美文网首页
文件上传与下载

文件上传与下载

作者: 欢西西西 | 来源:发表于2024-03-20 11:45 被阅读0次
new File([u8arr], filename, { type: 'image/jpeg' });

// Blob对象通常用于存储大量二进制数据,可以包含任意类型的数据
new Blob([data], { type: 'application/octet-stream; charset=utf-8' })

// Uint8Array是ArrayBuffer的一种视图类型,是一个类数组对象,
// 每项都是一个8 位无符号整数值,可以直接读取和写入二进制数据。
u8ay = new Uint8Array(arraybuffer);
u8ay[0] = 10
// u8ay.buffer === arraybuffer
  • BlobFile类型可以通过slice进行切片,通过FormData的搭载来上传到服务器
let formData = new FormData();
formData.append("Content-Disposition", `attachment; filename="${fileName}"`);
formData.append('key', key);
formData.append('file', file);
image.png
  • FileReader读取一个 txt格式的File对象(编码为UTF-8):
    readAsText.png
readAsArrayBuffer.png

1. 如何将input选择的图片展示在页面上

需要使用fileReader来转换文件:readAsDataURL

const reader = new FileReader();
reader.readAsDataURL(input.files[0]); // input.files 是一个FileList ,里面可以包含多个File文件
reader.onload = function (e) {
    console.log(e.target.result); // 拿到base64,再赋值给img节点的src
}

2. 分片上传 - 本文最下面有伪代码

分片上传视频

image.png
  1. slice将文件切分为多个Blob类型对象,存储在数组里
  2. 计算文件的哈希值,先询问服务端,该文件是否上传过,上传到第几片了
  3. 按分片顺序,循环-依次传给服务器(携带文件的哈希值,用以标识文件),同时携带当前处于第几分片
  4. 当最后一片上传成功后,发送一个结束上传的请求 - 携带文件哈希值- 后端返回文件地址

3. md5计算文件的哈希值——用以唯一标识文件

  • 思路是用FileReader将File readAsArrayBuffer,然后再对结果进行md5运算
  • 但是大文件不能一次性全部读到内存里来,所以要用切片数组来计算整个文件的哈希值,需要用到spark-md5,以通过增量的方式计算整个文件的md5值,这通常需要一些时间
  • 所以对文件哈希值的计算可能要使用Web Worker开新线程,避免页面卡住

4. 断点续传的触发节点是什么?

4.1. 选择文件上传,服务端检查发现此文件以前上传过一部分(可能原因:1. 之前上传过程中用户关闭页面 2. 之前上传过程中网络故障 3. 之前上传手动暂停)
4.2. 用户手动点了暂停,再点击继续,从暂停的部分继续上传
4.3. window监听online事件,发现有未完成的上传任务,继续上传(例如断网导致上传中断,前端记录了断点信息。但现在遇到的用户的操作一般不是单纯的上传,上传完后要拿到url去做后续的操作,例如把文件地址保存到某个单据上, 但如果断网导致上次的用户操作已经中断了,现在即使续传完毕,也不知道把拿到的url怎么办了。既然用户操作中断,下次应该会重新操作,在重新上传文件时触发第一种情况下的断点续传,我觉得这样更合理一点,而不是online时自动继续上传)

5. 上传进度

  • axios的onUploadProgress选项
  • web版本监听的是ajaxprogress事件
  • 客户端版本是调客户端方法进行的分片,回调里面通知前端 “文件的总大小、当前进度、文件的key”,当进度为100%的时候前端通过key获取文件在线地址
  • 自己做分片上传的进度就是已上传的size / 总size

6. 粘贴图片后上传

6.1 监听paste事件,获取clipboardData

el.addEventListener('paste', function (e) {
    const cbd = e.clipboardData;
    if (!(cbd && cbd.items)) {
        return;
    }
    let pasteItems = cbd.items;
}, false);

6.2 如果直接能获取到文件数据:getAsFile可以拿到file类型,此时就可以上传了,用 FileReader读取base64后也能展示到页面上

for (let i = 0, lth = pasteItems.length; i < lth; i++) {
    let item = pasteItems[i];
    if (item.kind === 'file') {
        let file = item.getAsFile(); // 第一步:getAsFile
        if (!file || file.size === 0) {
            continue;
        }
        // 第二步:这里再用 FileReader 来读这个 blob: readAsDataURL,结果就能作为 src 赋值给 img 了
    }
}
image.png

6.3 否则尝试以html格式解析文本:getAsStringnew DOMParser().parseFromString(a, 'text/html'),拿到内部img标签的src属性

image.png
for (let i = 0, lth = pasteItems.length; i < lth; i++) {
    pasteItems[i].getAsString(function (a) {
        if (!a) {
            return;
        }
        let nodes = new DOMParser().parseFromString(a, 'text/html'), // 这步很重要
            imgDomList = nodes.getElementsByTagName('img'); // 获取IMG DOM
        // 接下来就可以取 IMG 节点的 src 属性了
    });
}

从 img dom 取出来的 src 可能是:

  1. file:///C:/Users/cache/Image/2008208101184.png —— 【本地文件系统】的地址
    (本地路径我们是无法展示的,目前借助客户端,先把本地路径给客户端,客户端上传后返给我们在线链接)
  2. https://upload-images.jianshu.io/upload_images/1552a9bb5bd4.png —— 【在线】地址

7. 在浏览器地址栏输入文件地址时,为什么有的是预览,有的直接会下载

服务器可以通过设置响应头来指定文件的 MIME 类型和处理方式。如果服务器发送的响应头中包含了 "Content-Disposition: attachment",则浏览器会将其视为需要下载的文件,而不是直接打开预览。

image.png image.png

8. 为什么有的网站图片只能在这个网站内部才能访问?

有的网站设置了防盗链:

  • 例如简书通过请求头中的Referer来判断来源,不允许的来源就会返回 403 Forbidden(但Referer信息可以伪造,也可以不传,例如<meta name="referrer" content="no-referrer" />,没传的话就能访问)
  • 例如群晖是通过一个session ID来检查这个资源请求的合法性(这个sid是要用账号成功登录群晖服务器才能生成并返回的,默认放在cookie里,也存在 有效期),请求时拼接在【url地址上】或者【通过cookie】带过去(资源请求默认不会携带cookie),服务端校验通过才会返回正确资源。【如果你是通过携带sid参数拼接到链接上的方式去获取,短时别人拿你的请求地址也能访问,不过sid失效后就无法访问了】
  • 一个工具:在完整url前添加https://images.weserv.nl/?url=可以预览这个图片

9. 下载碰到过的一个问题

image.png

下载文件时发现含有中文逗号的无法下载。而这个报错码表示服务器在响应头部中设置了多个 Content-Disposition 字段。

原因:上传文件的时候会把到时下载要用的Content-Disposition头的值传给后端,但是由于filename没有包双引号,导致设置请求头的时候变成 a=xxxx,aaaa,文件名中的逗号当作属性分割了。

修改:上传时用双引号包上文件名,前后对比:

- image.png - image.png -

10. StreamSaver流式下载文件,并读取下载进度

(参见另一篇 service worker

const fileStream = streamSaver.createWriteStream(fileName); // WritableStream实例
const writer = fileStream.getWriter();
fetch(url).then(res => {
    const totalSize = parseInt(res.headers.get('content-length'));
    let loadedSize = 0;
    const readableStream = res.body;
    const reader = readableStream.getReader();
    reader.read().then(function processResult(result) {
        if (result.done) {
            writer.close(); // 下载完成
            return;
        }
        loadedSize += result.value.length;
        console.log(`下载进度:${loadedSize / totalSize * 100}%`);

        // 读过进度之后,可以继续写入可写流当中
        writer.write(result.value).catch(error => {
            writer.close();
        });
        return reader.read().then(processResult);
    });
})

11. 分片上传伪代码

const STATE_UPLOADING = 'sending', // 正在上传
    STATE_STOP = 'stop', // 各种原因导致的中断
    STATE_SUCCESS = 'success'; // 成功

const CHUNK_SIZE = 10 * 1024 * 1024; // 切成10M
// const CHUNK_SIZE = 10 * 1024; // 切成10K
const uploadFile = Symbol('uploadFile');
const continueUpload = Symbol('continueUpload');
const compPort = Symbol('componentPort');
const changeState = Symbol('changeState');
const handleUploadFail = Symbol('handleUploadFail');
const updatePercent = Symbol('updatePercent');

const tools = {
    calcSign(file) { },
    getFileUrl(fileSign, fileSize) {  },
    postToServer(fileSign, chunkIndex, chunkFile, signal) { },
    checkFileFromServer(fileSign) { }
};

export default class Upload {
    state = STATE_UPLOADING
    stopChunkIndex = 0
    fileSign = null // 真正用的地方就continueUpload一个,调用位置在:初始化、从暂停处继续
    file = null
    fileUrl = ''
    messagePort = null
    percent = 0
    constructor(f, options = {}) {
        this.file = f;
        this.fileSign = options.fileSign || tools.calcSign(f);
        if (options.startIndex) {
            this.stopChunkIndex = options.startIndex;
        }
        this[uploadFile]();

        // 接下来用于实例之间通信:去通知实例上传状态发生变化、进度发生变化
        // 实例需要知道的话就用messagePort去监听message事件
        const { port1, port2 } = new MessageChannel();
        this[compPort] = port1;
        this.messagePort = port2;
    }
    // 暂停上传
    suspend() {
        this[changeState](STATE_STOP);
        this.abortController.abort();
    }
    // 继续上传
    proceed() {
        if (this.state === STATE_STOP) {
            this[changeState](STATE_UPLOADING);
            this[continueUpload]().catch((...args) => {
                this[handleUploadFail](...args);
            });
        }
    }
    // 上传文件,成功则resolve url,失败会reject error
    async [uploadFile]() {
        // 检查这个文件是否上传过
        let res = await tools.checkFileFromServer(this.fileSign);

        // 已上传完毕:秒传
        if (res.url) {
            this.fileUrl = res.url;
            this[updatePercent](100);
            this[changeState](STATE_SUCCESS);
            return res.url;
        }
        // 上传了一部分:从上次位置续传
        if (res.uploadedChunkCount > 0) {
            this.stopChunkIndex = res.uploadedChunkCount;
        }
        // 没传过:传
        return this[continueUpload]().catch((...args) => {
            this[handleUploadFail](...args);
        });
    }
    async [continueUpload]() {
        this.abortController = new AbortController();
        const startIndex = this.stopChunkIndex;
        const fileSize = this.file.size;
        let transferSize = startIndex * CHUNK_SIZE, // 已上传的部分
            chunkIndex = startIndex;
        let isErr = false;
        while (transferSize < fileSize) {
            if (this.state === STATE_STOP) { // 暂停
                this.stopChunkIndex = chunkIndex;
                return Promise.reject({ code: 300 });
            }
            let chunkFile = this.file.slice(transferSize, transferSize + CHUNK_SIZE);
            try {
                let res = await tools.postToServer(this.fileSign, chunkIndex, chunkFile, this.abortController.signal);
                if (res.ok) {
                    transferSize += chunkFile.size;
                    chunkIndex++;
                    this[updatePercent](transferSize / fileSize * 100);
                } else {
                    isErr = true;
                    break;
                }
            } catch (err) {
                console.log('fetch error:', err);
                isErr = true;
                break;
            }
        }
        if (isErr) { // 分片上传中断
            this.stopChunkIndex = chunkIndex;
            return Promise.reject({ code: 100 });
        }
        // 上传完成,获取下载地址
        return tools.getFileUrl(this.fileSign, fileSize).then(url => {
            this.fileUrl = url;
            this[changeState](STATE_SUCCESS);
            return url;
        }).catch(() => {
            return Promise.reject({ code: 400, });
        });
    }
    [changeState](state) {
        if (this.state === state) {
            return;
        }
        // 成功了就不能再变更状态了
        if (this.state === STATE_SUCCESS) {
            return;
        }
        this.state = state;
        this[compPort].postMessage({ action: 'statechange', state });
    }
    // 该文件的上传进度
    [updatePercent](value) {
        value = parseInt(value);
        value = Math.min(value, 100);
        this[compPort].postMessage({ action: 'progress', value, text: value + '%' });
    }
    // 统一处理上传失败
    [handleUploadFail](err) {
        const errInfo = {
            '100': '分片上传中断',
            '400': '已全部上传,但获取文件地址失败',
            '300': '用户暂停上传'
        };
        this[changeState](STATE_STOP);
        if (err.code === 100) {
            //  存到localStorage,便于下次续传,记录file、fileSign、断点位置
            //  在合适的节点监听online-如果localStorage有未完成的任务则继续上传 -> new Upload(file, {fileSign, startIndex})
            // localStorage.setItem();
            return;
        }
    }
};

12. 遇到的问题记录:

问题1:通知实例状态的更新和进度的更新
→ 现在用的MessageChannel,给实例一个port让它自己去监听messgae

问题2:点了暂停之后进度条还会再更新一次
→ 因为我是在下一个分片发送前才校验并停止发送请求,所以即使点了暂停,本次分片还是会上传完,并且更新一次进度。所以如果在最后一个分片正在上传时点暂停,实际上是没有用的,你看着进度条没满的时候点的暂停,但上传结束了。
→ 所以点暂停时应该abort当前正在发生的请求(给请求一个abortController.signal,在点击暂停时调用abortController.abort()

相关文章

  • 常用命令

    文件上传与下载 上传:scp 文件路径 用户名@主机IP:上传路径 下载:scp 用户名@主机IP:文件路径 保存...

  • Linux文件管理-----day05

    .文件管理之-------- 下载文件(wget、curl)、文件上传与下载(rz、sz) 一、---------...

  • java文件下载上传

    文件上传 App图片上传 html文件上传 文件下载 文件下载 版本控制 xml版本控制

  • SSH上传下载命令

    上传文件 下载文件 上传目录 下载目录

  • SpringBoot基础教程(十一) | 文件篇

    文件上传与下载 在实际项目中,经常需要文件的上传和下载功能,如果使用Apache的Commons-fileuplo...

  • SpringBoot中文件上传与下载&swagger的使

    SpringBoot中文件上传与下载&swagger的使用 一、文件上传 单元测试: Controller: 二、...

  • springMVC的文件上传下载功能实现

    各位小伙伴 今天咱们分享的就是 文件上传与下载 文件的上传 单个文件的上传 文件上传默认是采用的apache 的上...

  • 文件上传与下载

    Servlet实现文件上传与下载 所需包:commons-fileupload-1.2.1.jarcommons-...

  • 文件上传与下载

    文件的上传 1.首先需要俩个依赖包 2.文件上传的页面 表单需要用input标签,type为file,enctyp...

  • 文件上传与下载

    文件上传和下载 文件上传和下载是JAVA WEB中常见的一种操作,文件上传主要是将文件通过IO流传输到服务器的某一...

网友评论

      本文标题:文件上传与下载

      本文链接:https://www.haomeiwen.com/subject/eydjldtx.html