草庐IT

前端大文件分段上传;控制接口并发数量

家有饿犬和聋猫 2023-09-13 原文

说明:使用axios方式上传,文件不能过大,因为过多的连续Ajax请求会使后台崩溃,接口报错;所以使用分段上传的方式,减轻服务器的压力。其实就是将 文件变小,也就是通过 文件资源分块 后再上传。

问题 1:谁负责资源分块?谁负责资源整合?
前端负责分块,服务端负责整合.

问题 2:前端怎么对资源进行分块?
首先是选择上传的文件资源,接着就可以得到对应的文件对象 File,而 File.prototype.slice 方法可以实现资源的分块,当然也有人说是 Blob.prototype.slice 方法,因为 Blob.prototype.slice === File.prototype.slice.

问题 3:服务端怎么知道什么时候要整合资源?如何保证资源整合的有序性?
由于前端会将资源分块,然后单独发送请求,也就是说,原来 1 个文件对应 1 个上传请求,现在可能会变成 1 个文件对应 n 个上传请求,所以前端可以基于 Promise.all 将这多个接口整合,上传完成在发送一个合并的请求,通知服务端进行合并。

在发送请求资源时,前端会定好每个文件对应的序号,并将当前分块、序号以及文件 hash 等信息一起发送给服务端,服务端在进行合并时,通过序号进行依次合并即可。

此示例是纯前端代码,不涉及后端。
录屏.gif
第一步:使用input或者antd_upload获取文件
image.png
第二步:调接口获取文件段数,分段列表和分段尺寸;使用slice方法,分段读取文件为blob
   let dataMsg = await createMultipart({
       fileSize: file.size, // 传参数
       filename: file.name
   }).then(
       (rem) => {
           return rem.data;
       },
       (err) => {
           return upFailed(file, onUpload); // 如果接口报错,使用upFailed方法处理
       }
   );
   let urlList = dataMsg?.parts || []; // 分段列表
   let DEFAULT_SIZE = dataMsg?.partSize; // 分段尺寸

   for (let i = 0; i < urlList.length; i++) {
       let url = urlList[i]['url'];
       let fname = encodeURIComponent(file.name);
       let start = i * DEFAULT_SIZE;
       let stepFile;
       if (i === urlList.length - 1) {
          // 使用slice方法,分段读取文件为blob 
           stepFile = file.slice(start, -1); // 如果是最后一段,直接截取剩下的所有内容
       } else {
           stepFile = file.slice(start, start + DEFAULT_SIZE); // 分割文件
       }
       urlList[i]['stepFile'] = stepFile;
       urlList[i]['fname'] = fname;
       urlList[i]['uid'] = file.uid;
   }

urlList已准备好

image.png

数据说明: {
fname: '使用encodeURIComponent 编码过的文件名',
partNumber: '段数序号,合并时候使用',
stepFile: '截取的文件'
uid: 'antd组件生成的文件唯一值',
url: '上传该段文件的路径'
};

第三步:循环urlList,上传每一段文件

准备工作:单个文件上传方法

 const detalItem = ({ url, stepFile, fname, partNumber }) => {
        return new Promise((resolve, reject) => {
            fileAxios({
                url,
                method: 'PUT',
                data: stepFile,
                headers: {
                    'Content-Type': '',
                    'Content-disposition': `filename*=utf-8\'zh_cn\'${fname}`
                }
            })
                .then((res) => {
                    let str = res.headers.etag.split('"').join('');
                    resolve({ eTag: str, partNumber });
                })
                .catch((err) => {
                    reject({ eTag: '', partNumber });
                });
        });
    };


准备工作:并发上传,控制每次上传的接口数量,防止上传接口数量过多,浏览器崩溃。

参数说明:
poolLimit(数字类型):表示限制的并发数;
array(数组类型):表示任务数组;
iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数;
onUpload: 进度条

async function asyncPool(poolLimit, array, iteratorFn, onUpload) {
    const ret = []; // 存储所有的异步任务
    const executing = []; // 存储正在执行的异步任务
    for (const item of array) {
        // 结束运行
        if (endExecution.end && endExecution.uid === item?.uid) {
            return;
        }

       --------重点开始---------------
        // 调用iteratorFn函数创建异步任务
        const p = Promise.resolve().then(() => iteratorFn(item, array));
        ret.push(p); // 保存新的异步任务

        // 当poolLimit值小于或等于总任务个数时,进行并发控制
        if (poolLimit <= array.length) {
            // 当任务完成后,从正在执行的任务数组中移除已完成的任务
            const e = p.then(() => executing.splice(executing.indexOf(e), 1));
            executing.push(e); // 保存正在执行的异步任务
            if (executing.length >= poolLimit) {
                await Promise.race(executing); // 等待较快的任务执行完成
            }
        }
       --------重点结束---------------

       // 进度条
        onUpload &&
            onUpload({
                loaded: ret.length < array.length ? ret.length : ret.length - 1, // 等到接口合并完成,再返回100%
                total: array.length,
                uid: item['uid'],
                endAction: endAction // 如果用户删除文件,调用此函数,结束文件上传
            }); // 进度条
    }
    return Promise.all(ret);  // 集合多个返回结果
}

整合方法,开始上传

 let etags = [];
    try {
        etags = await asyncPool(5, urlList, detalItem, onUpload);  // 重点
    } catch {
        // 上传失败
        etags = [];
        endExecution.end = true;
        endExecution.uid = file?.uid;
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return upFailed(file, onUpload);
    }

请求中,保证5条并发数,如果5条中有请求结束了,自动补上


image.png

创建请求,请求全部发出,结束后合并文件


image.png

文件上传完的结果,etags。
eTag是每段文件的唯一值,
partNumber: 文件顺序。后端根据这个数据表来合并文件,避免顺序乱了。


image.png
第四步: 通知后端,合并文件
 let params = {
        attachmentID: dataMsg?.attachmentID,
        uploadID: dataMsg?.uploadID
    };

    if (endExecution.end && endExecution.uid === file?.uid) {
        console.log('删除文件,结束上传,调用结束上传接口,后端清除已经上传的数据');
        cancelMultipart(params);
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return { file: file, upResult: '' };
    }
      --------重点开始---------------
      // 调接口 传参数
      result = await completeMultipart({
            ...params,
            etags
        });
        if (etags) {
            onUpload &&
                onUpload({
                    loaded: 100,
                    total: 100,
                    uid: file['uid'],
                    endAction: endAction
                }); // 进度条
        }
        let presignedURL = result?.data?.presignedURL;
        // console.log('result', result, 'presignedURL', presignedURL);
        file['url'] = presignedURL;
        file['link'] = presignedURL;
        file['attachmentID'] = dataMsg?.attachmentID;
        file['status'] = 'done';

        --------重点结束---------------
        return { file: file, upResult: '' };

附加功能:
1 返回进度条onUpload,原理: 当前发出去的请求数,除以总条数
2 结束请求endAction,应用场景,文件正在上传中,删除文件,结束接口调用

全部代码:

React上传组件:

import React, { Component } from 'react';
import { Upload, Progress, Tooltip, Modal } from 'antd';
const { Dragger } = Upload;
export default class List extends Component {
    constructor(props) {
        super(props);
        this.state = {
            fileList: [
                // {
                //     uid: '-1',
                //     name: 'image.png',
                //     status: 'done',
                //     url:
                //         'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
                // }
            ],
            visible: false,
            upLoading: false
        };
        this.formRef = React.createRef();
        this.littleRef = React.createRef();
        this.departmentRef = React.createRef();
        this.smalledepartmentRef = React.createRef();
    }
 // 进度条
  handleProgress = (progressEvent) => {
        const num = (progressEvent.loaded / progressEvent.total) * 100;
        let percent = num >= 100 ? 100 : num.toFixed(2) * 1;
        const { fileList } = this.state;

        this[`${progressEvent?.uid}_up`] = progressEvent;

        // if (progressEvent.loaded > 5) {
        //     progressEvent.endAction();
        // }

        this.setState({
            fileList: fileList.map((p) => {
                if (p?.uid === progressEvent?.uid) {
                    p['percent'] = percent;
                }
                return p;
            })
        });
    };
    // 删除文件
    onRemove = (file) => {
        if (!file?.status) {
            // 删除正在上传的文件,结束调用
            this[`${file?.uid}_up`] &&
                this[`${file?.uid}_up`]?.endAction &&
                this[`${file?.uid}_up`]?.endAction(file?.uid);
        }
        const { fileList } = this.state;
        let newList = fileList.filter((p) => p.uid !== file.uid);
        let loading = false;
        for (let v of newList) {
            if (!v?.status) {
                loading = true;
                break;
            }
        }
        this.setState({
            fileList: newList,
            upLoading: loading
        });
    };
    beforeUpload = (file, fileLists) => {
        console.log('打印file:', file);
        let repeat = [...this.state.fileList, ...fileLists];
        let obj = {};
        let noRepeat = repeat.reduce((pur, item) => {
            if (!obj[item?.uid]) {
                obj[item?.uid] = true;
                pur.push(item);
            }
            return pur;
        }, []);
        this.setState({ fileList: noRepeat, upLoading: true });
        commonUpload({ file, onUpload: this.handleProgress })
            .then((rem) => {
                const { fileList } = this.state;
                var data = {};
                for (var key in rem.file) {
                    data[key] = rem.file[key];
                }

                let newFilelist = fileList
                    .map((p) => {
                        if (p) {
                            if (p?.uid === data?.['uid']) {
                                p = { ...p, ...data };
                            }
                            return p;
                        }
                    })
                    .filter((p) => p?.status !== 'error');
                if (isNotEmpty(rem.file)) {
                    this.setState({
                        fileList: newFilelist
                    });
                }
            })
            .finally(() => {
                const { fileList } = this.state;
                // 批量上传完成,关闭loading
                let flag = true;
                for (let item of fileList) {
                    if (!item?.status) {
                        flag = false;
                        break;
                    }
                }
                flag && this.setState({ upLoading: false });
                // console.log('this.state.fqwFile', JSON.parse(this.state.fqwFile));
            });
        // 阻止默认上传
        return false;
      };
    render(){
             <Dragger
                           fileList={fileList}
                          className="drag-uploader"
                            onPreview={this.handlePreview} // 点击文件链接或预览图标时的回调
                            onRemove={this.onRemove}
                            multiple={true} // 支持多个文件一起上传
                          // onChange={this.onfileChange}
                          itemRender={(originNode, file, currFileList) => (
                           <UploadListItem
                                            originNode={originNode}
                                            file={file}
                                            currFileList={currFileList}
                                            fileList={fileList}
                                        />
                                    )}
                                    beforeUpload={this.beforeUpload}
                                    showUploadList={{
                                        showPreviewIcon: false,
                                        downloadIcon: true
                                    }}
                          >
                                    {fileList.length >= 15 ? null : UploadButton}
                          </Dragger>
}


进度条uploadListItem.jsx文件

/*
 * @desc   文件上传,自定义上传列表项, 带进度条
 * @author fqw
 */

import React, { Component } from 'react';
import { Progress, Tooltip } from 'antd';
import Cns from 'classnames';
import './index.scss';

const UploadListItem = ({ originNode, file, current, fileList }) => {
    const errorNode = <Tooltip title={file['response']}>{originNode.props.children}</Tooltip>;
    let have = file.percent < 100;
    return (
        <div
            className={Cns('ant-upload-draggable-list-item', have && 'progressIng')}
            style={{ cursor: 'move' }}
            key={file.percent}
        >
            {file.status === 'error' ? errorNode : originNode}
            {have && <Progress style={{ width: '100px' }} percent={file.percent} />}
        </div>
    );
};
export default UploadListItem;

fileAxios.js文件

import { message } from 'antd';
import axios from 'axios';
import { cancelMultipart } from './common';
import {
    getUserPresignedurl,
    submitFileMsg,
    createMultipart,
    completeMultipart
} from 'services/common';

// 结束运行
let endExecution = {
    end: false,
    uid: ''
};
let endAction = (uid) => {
    endExecution.end = true;
    endExecution.uid = uid;
};
let upFailed = (file, onUpload) => {
    endExecution.end = true;
    endExecution.uid = file?.uid;
    file['status'] = 'error';
    file['response'] = '上传失败,请重试';
    message.warning({
        content: `文件 ${file.name} 上传失败,请重试`,
        duration: 5
    });
    onUpload &&
        onUpload({
            loaded: 1, // 结束进度条,不显示
            total: 1,
            uid: file['uid'],
            endAction: endAction
        }); // 进度条
    return { file, upResult: false };
};
// 普通上传
const uploadFile = async (file, onUpload) => {
    // 获取上传接口的路径
    let urlRest = await getUserPresignedurl({ filename: file.name, fileSize: file.size }).then(
        (rem) => {
            if (rem.status === 200) {
                // file['uid'] = rem.data['attachmentID'];
                file = Object.assign(file, rem.data);
                return rem.data;
            }
        }
    );
    // 获取文件类型
    let fileType = file.name.split('.').slice(-1)[0];
    let typesObj = {
        jpg: 'image/jpeg',
        jpe: 'image/jpeg',
        jpeg: 'image/jpeg',
        png: 'image/png',
        gif: 'image/gif',
        bmp: 'application/x-bmp',
        wbmp: 'image/vnd.wap.wbmp',
        ico: 'image/x-icon',
        pdf: 'application/pdf',
        ppt: 'application/x-ppt',
        doc: 'application/msword',
        xls: 'application/vnd.ms-excel'
    };
    let url = urlRest.presignedURL;
    const fileAxios = axios.create();
    let fname = encodeURIComponent(file.name);
    let upBool = false;
    upBool = await fileAxios({
        url,
        method: 'PUT',
        data: file,
        headers: {
            'Content-Type': typesObj[fileType] || '',
            'Content-disposition': `filename*=utf-8\'zh_cn\'${fname}`
        },
        onUploadProgress: (arg) => {
            arg.uid = file.uid;
            onUpload(arg);
        }
    })
        .then((res) => {
            return res.status === 200;
        })
        .catch((err) => {
            return false;
        });
    // 上传失败,结束运行
    if (!upBool) {
        return upFailed(file, onUpload);
    }
    // 获取文件下载或预览链接
    let upResult = await submitFileMsg({
        filename: file.name,
        fileSize: file.size,
        attachmentID: urlRest.attachmentID
    }).then(
        (rem) => {
            file['url'] = rem.data['link'];
            file['status'] = 'done';
            file['attachmentID'] = urlRest.attachmentID;
            file = Object.assign(file, rem.data);
            return true;
        },
        (err) => {
            return false;
        }
    );
    let copy = JSON.parse(JSON.stringify(file));
    copy['name'] = file.name;
    return { file: copy, upResult };
};

// poolLimit(数字类型):表示限制的并发数;
// array(数组类型):表示任务数组;
// iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数
// onUpload: 进度条

async function asyncPool(poolLimit, array, iteratorFn, onUpload) {
    const ret = []; // 存储所有的异步任务
    const executing = []; // 存储正在执行的异步任务
    for (const item of array) {
        // 结束运行
        if (endExecution.end && endExecution.uid === item?.uid) {
            console.log('结束上传0');
            return;
        }
        // 调用iteratorFn函数创建异步任务
        const p = Promise.resolve().then(() => iteratorFn(item, array));
        ret.push(p); // 保存新的异步任务

        // 当poolLimit值小于或等于总任务个数时,进行并发控制
        if (poolLimit <= array.length) {
            // 当任务完成后,从正在执行的任务数组中移除已完成的任务
            const e = p.then(() => executing.splice(executing.indexOf(e), 1));
            executing.push(e); // 保存正在执行的异步任务
            if (executing.length >= poolLimit) {
                await Promise.race(executing); // 等待较快的任务执行完成
            }
        }

        onUpload &&
            onUpload({
                loaded: ret.length < array.length ? ret.length : ret.length - 1, // 等到接口合并完成,再返回100%
                total: array.length,
                uid: item['uid'],
                endAction: endAction
            }); // 进度条
    }
    return Promise.all(ret);
}

// 分段上传
const multiPartUpload = async (file, onUpload = null) => {
    // 获取段数
    let dataMsg = await createMultipart({
        fileSize: file.size, // 传参数
        filename: file.name
    }).then(
        (rem) => {
            return rem.data;
        },
        (err) => {
            return upFailed(file, onUpload); // 如果接口报错,使用upFailed方法处理
        }
    );
    let urlList = dataMsg?.parts || []; // 分段列表
    let DEFAULT_SIZE = dataMsg?.partSize; // 分段尺寸

    for (let i = 0; i < urlList.length; i++) {
        let url = urlList[i]['url'];
        let fname = encodeURIComponent(file.name);
        let start = i * DEFAULT_SIZE;
        let stepFile;
        if (i === urlList.length - 1) {
            stepFile = file.slice(start, -1); // 如果是最后一段的话,直接截取剩下的所有内容
        } else {
            stepFile = file.slice(start, start + DEFAULT_SIZE); // 分割文件
        }
        urlList[i]['stepFile'] = stepFile;
        urlList[i]['fname'] = fname;
        urlList[i]['uid'] = file.uid;
    }

    const fileAxios = axios.create();

    const detalItem = ({ url, stepFile, fname, partNumber }) => {
        return new Promise((resolve, reject) => {
            fileAxios({
                url,
                method: 'PUT',
                data: stepFile,
                headers: {
                    'Content-Type': '',
                    'Content-disposition': `filename*=utf-8\'zh_cn\'${fname}`
                }
            })
                .then((res) => {
                    let str = res.headers.etag.split('"').join('');
                    resolve({ eTag: str, partNumber });
                })
                .catch((err) => {
                    reject({ eTag: '', partNumber });
                });
        });
    };

    let etags = [];
    try {
        etags = await asyncPool(5, urlList, detalItem, onUpload);
    } catch {
        // 上传失败
        etags = [];
        endExecution.end = true;
        endExecution.uid = file?.uid;
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return upFailed(file, onUpload);
    }
    let params = {
        attachmentID: dataMsg?.attachmentID,
        uploadID: dataMsg?.uploadID
    };

    if (endExecution.end && endExecution.uid === file?.uid) {
        cancelMultipart(params);
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return { file: file, upResult: '' };
    }

    let result = null;
    // console.log('etags', etags);
    // 上传完合并文件
    try {
        result = await completeMultipart({
            ...params,
            etags
        });
        if (etags) {
            onUpload &&
                onUpload({
                    loaded: 100,
                    total: 100,
                    uid: file['uid'],
                    endAction: endAction
                }); // 进度条
        }
        let presignedURL = result?.data?.presignedURL;
        // console.log('result', result, 'presignedURL', presignedURL);
        file['url'] = presignedURL;
        file['link'] = presignedURL;
        file['attachmentID'] = dataMsg?.attachmentID;
        file['status'] = 'done';
    } catch {
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return upFailed(file, onUpload);
    }
    let copy = JSON.parse(JSON.stringify(file));
    copy['name'] = file.name;
    return { file: copy, upResult: '' };
};

export const commonUpload = ({ file, onUpload }) => {
    let fileSize = 100; // 100M
    if (file.size / 1024 / 1024 > fileSize) {
        // 当文件大于100M采用分段上传;
        return multiPartUpload(file, onUpload);
    } else {
        return uploadFile(file, onUpload);
    }
};

有关前端大文件分段上传;控制接口并发数量的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐