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

1. 需求描述

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

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

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

/**
    * @param downloadFilename 下载压缩文件的名称
     * @param downloads        要下载的音频集合
     * @param response         response
     * @description: 压缩包文件流下载
     */
public void downloadZip(String downloadFilename, List 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 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 downloads) {
    Set 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再去下载。

你可能感兴趣的:(文件打成压缩包通过上传OSS给前端下载功能实现)