文件打成压缩包通过上传OSS给前端下载功能实现

1. 需求描述

前端通过正则识别出音频文件URL传给后端,后端打成zip文件给前端下载,需要考虑稳定性和下载速度。

2. 实现一:直接通过流读取压缩和返回前端

直接通过流接受并转为 zipOutStream 流写到 response 里给前端。

/**
    * @param downloadFilename 下载压缩文件的名称
     * @param downloads        要下载的音频集合
     * @param response         response
     * @description: 压缩包文件流下载
     */
public void downloadZip(String downloadFilename, List<AudioDownloadDto> downloads, HttpServletResponse response) {
    if (ListUtils.isEmpty(downloads)) {
        return;
    }
    dealRepeatFileName(downloads);
    ZipOutputStream zos = null;
    try {
        downloadFilename = URLEncoder.encode(downloadFilename, "UTF-8");
        // 指明response的返回对象是文件流
        response.setContentType("application/octet-stream");
        // 设置在下载框默认显示的文件名
        response.setHeader("Content-Disposition", "attachment;filename=" + downloadFilename);
        zos = new ZipOutputStream(response.getOutputStream());
        for (AudioDownloadDto download : downloads) {
            packageZipOutPutStream(zos, download);
        }
        zos.finish();
    } catch (Exception e) {
        log.error("下载音频压缩包失败 :{}", e.getMessage());
        throw new ServiceException("下载音频压缩包失败");
    } finally {
        try {
            if (zos != null) {
                zos.close();
            }
        } catch (IOException e) {
            log.error("ZipOutputStream close fail :{}", e.getMessage());
            throw new ServiceException("ZipOutputStream close fail");
        }
    }
}

packageZipOutPutStream 方法:

/**
     * @param zos
     * @param download
     * @return
     * @throws IOException
     * @description: 转为压缩流
     */
private boolean packageZipOutPutStream(ZipOutputStream zos, AudioDownloadDto download) throws IOException {
    String fileUrl = download.getUrl();
    ZipEntry zipEntry = new ZipEntry(getFileName(download.getQueId(), fileUrl));
    zos.putNextEntry(zipEntry);
    InputStream fis = FileUtils.getInputStreamByFileUrl(fileUrl);
    // 因为 URL 为 OSS 地址,故此处也可以直接从通过 OSS API 获取要下载的内容(走内网的话其两种方式的效率差不多)
    /*OSSClient ossClient=  ossUtil.getOSSClient();
    OSSObject ossObject = ossClient.getObject(ossPropResource.getUserStorageOssBucketName(), StringUtils.substringAfterLast(fileUrl, ".com/"));
    InputStream fis = ossObject.getObjectContent();*/
    if (fis == null) {
        return true;
    }
    byte[] buffer = new byte[2048];
    int r;
    while ((r = fis.read(buffer)) != -1) {
        zos.write(buffer, 0, r);
    }
    fis.close();
    //  注意写完一个文件,需要关闭这个文件,再写下一个
    zos.closeEntry();
    zos.flush();
    return false;
}

3. 实现二:先上传 OSS,把返回的压缩文件地址给前端

中间不需要落地文件,直接通过流写入 OSS 压缩文件,返回oss地址给前端下载,后续通过定时任务把生成的oss 压缩文件删除,节约oss空间资源

/**
     * @param downloadFilename 下载压缩文件的名称
     * @param downloads        要下载的音频集合
     * @return
     * @description: 获取压缩包oss地址下载
     */
public String queryDownloadZipOSSUrl(String downloadFilename, List<AudioDownloadDto> downloads) {
    if (ListUtils.isEmpty(downloads)) {
        return null;
    }
    dealRepeatFileName(downloads);
    ZipOutputStream zos = null;
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try {
        zos = new ZipOutputStream(bos);
        for (AudioDownloadDto download : downloads) {
            packageZipOutPutStream(zos, download);
        }
        zos.finish();
        InputStream inputStream = new ByteArrayInputStream(bos.toByteArray());
        String key = UUID.randomUUID().toString().replace("-", "") + Constants.FileSuffix.ZIP;
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType("application/octet-stream");
        metadata.setContentDisposition("attachment;filename=" + downloadFilename);
        ossService.pubObjectStream(ossPropResource.getTiKuBucketName(), SUB_BUCKET_NAME + key, inputStream, metadata);
        // 放入redis队列等待定时任务删除
        redisDao.putListObject(Constants.Cache.AUDIO_ZIP_OSS_KEY_LIST, key);
        return ossPropResource.getTiKuBucketEndpoint() + "/" + SUB_BUCKET_NAME + key;
    } catch (Exception e) {
        log.error("获取音频压缩文件的OSS地址失败 :{}", e.getMessage());
        throw new ServiceException("获取音频压缩文件的OSS地址失败");
    } finally {
        try {
            if (zos != null) {
                zos.close();
            }
        } catch (IOException e) {
            log.error("ZipOutputStream close fail :{}", e.getMessage());
            throw new ServiceException("ZipOutputStream close fail");
        }
    }
}

涉及到的一些方法:

/**
     * @description: 文件名重复问题
     * @author: zcq
     * @date: 2020/11/27 4:44 下午
     */
public static void dealRepeatFileName(List<AudioDownloadDto> downloads) {
    Set<String> queIdSet = new HashSet();
    for (AudioDownloadDto download : downloads) {
        if (!queIdSet.add(download.getQueId())) {
            // 试题重复,通过增加时间戳来去重
            download.setQueId(download.getQueId() + "-" + UUID.randomUUID().toString().replace("-", ""));
        }
    }
}

/**
     * @description: 根据文件路径获取文件的扩展名
     * @author: zcq
     * @date: 2020/11/26 6:18 下午
     */
private static String getFileName(String queId, String url) {
    return queId + Constants.Decollator.POINT_DECOLLATOR + StringUtils.substringAfterLast(url, Constants.Decollator.POINT_DECOLLATOR);
}

/**
     * @description: 删除OSS文件
     * @author: zcq
     * @date: 2020/12/3 3:33 下午
     */
public void delOssFile(final String ossKey) {
    ossService.deleteObject(ossPropResource.getTiKuBucketName(), SUB_BUCKET_NAME + ossKey);
}

4. 总结

第二种方式为更优的选择,oss通过内网上传效率很高,节省了流给前端传输的时间,并且稳定性也更好。其次,下载压力从服务器端移到了阿里云oss,降低了服务器的压力。

其实还可以通过队列异步处理下载压缩请求。前端请求下载传url集合,后端放到队列之后直接返回成功(每一次下载任务生成唯一ID返给前端)。然后后端队列任务异步处理,前端可以轮询获取拿着唯一ID获取处理成功返回的URL再去下载。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容