import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; @Slf4j public class SliceUtil { /** * 分片大小 */ public final static long PER_PAGE = (long) 1024 * 1024; private static final RestTemplate REST_TEMPLATE = new RestTemplate(); /** * 根据分片下载 * * @param downloadUrl * @param start * @param end * @return */ public static ResponseEntity<byte[]> getFileContentByUrlAndPosition(String downloadUrl, long start, long end) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Range", "bytes=" + start + "-" + end); org.springframework.http.HttpEntity<Object> httpEntity = new org.springframework.http.HttpEntity<>(httpHeaders); return REST_TEMPLATE.exchange(downloadUrl, HttpMethod.GET, httpEntity, byte[].class); } /** * 下载 * * @param tempPath * @param downloadUrl * @param sliceInfo * @param fName */ public static void download(String tempPath, String downloadUrl, SliceInfo sliceInfo, String fName) { log.info("下载分片文件:{},分片序号 {}", fName, sliceInfo.getPage()); // 创建一个分片文件对象 File file = new File(tempPath, sliceInfo.getPage() + "-" + fName); if (file.exists() && file.length() == PER_PAGE) { log.info("此分片文件 {} 已存在", sliceInfo.getPage()); return; } try (FileOutputStream fos = new FileOutputStream(file);) { ResponseEntity<byte[]> responseEntity = SliceUtil.getFileContentByUrlAndPosition(downloadUrl, sliceInfo.getStart(), sliceInfo.getEnd()); byte[] body = responseEntity.getBody(); if (body != null && body.length == 0) { log.warn("分片文件:{},没有内容", file.getName()); return; } // 将分片内容写入临时存储分片文件 fos.write(body); } catch (IOException e) { e.printStackTrace(); } } /** * 合并文件 * * @param tempPath * @param fName * @param page */ public static void mergeFileTranTo(String tempPath, String fName, long page) { try (FileChannel channel = new FileOutputStream(new File(tempPath, fName)).getChannel()) { for (long i = 1; i <= page; i++) { File file = new File(tempPath, i + "-" + fName); FileChannel fileChannel = new FileInputStream(file).getChannel(); long size = fileChannel.size(); for (long left = size; left > 0; ) { left -= fileChannel.transferTo((size - left), left, channel); } fileChannel.close(); file.delete(); } } catch (IOException e) { e.printStackTrace(); } } }
/** * 分片页信息 */ @Data public class SlicePageInfo { private CopyOnWriteArrayList<SliceInfo> sliceInfoList; private Long page; }
import lombok.AllArgsConstructor; import lombok.Data; /** * 文件分片信息 */ @Data @AllArgsConstructor public class SliceInfo { private long start; private long end; private long page; }
import com.ruoyi.common.utils.spring.SpringUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import static com.ruoyi.download.slice.SliceUtil.PER_PAGE; @Slf4j public class DownLoadEngine { // 原生线程池 // private static final ExecutorService executorService = ExecutorFactory.newFixedExecutorService(5); // 若依线程池 private static ThreadPoolTaskExecutor executorService = SpringUtils.getBean("threadPoolTaskExecutor"); /** * 分片下载 * * @param downloadUrl 下载链接 * @param tempPath 临时文件路径 * @param fileName 文件名称 */ public static void downloadSlice(String downloadUrl, String tempPath, String fileName) { //大小探测 ResponseEntity<byte[]> responseEntity = SliceUtil.getFileContentByUrlAndPosition(downloadUrl, 0, 1); HttpHeaders headers = responseEntity.getHeaders(); String rangeBytes = headers.getFirst("Content-Range"); if (Objects.isNull(rangeBytes)) { log.error("url:{},不支持分片下载", downloadUrl); return; } long allBytes = Long.parseLong(rangeBytes.split("/")[1]); log.info("文件总大小:{}M", allBytes / 1024.0 / 1024.0); //分页 SlicePageInfo slicePageInfo = splitPage(allBytes); CountDownLatch countDownLatch = new CountDownLatch(Math.toIntExact(slicePageInfo.getPage())); CountDownLatch mainLatch = new CountDownLatch(1); executorService.execute(() -> { try { countDownLatch.await(); SliceUtil.mergeFileTranTo(tempPath, fileName, slicePageInfo.getPage()); mainLatch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }); for (SliceInfo sliceInfo : slicePageInfo.getSliceInfoList()) { executorService.submit(() -> { SliceUtil.download(tempPath, downloadUrl, sliceInfo, fileName); countDownLatch.countDown(); }); } try { mainLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 文件分片 * * @param allBytes 文件总大小 * @return / */ public static SlicePageInfo splitPage(long allBytes) { CopyOnWriteArrayList<SliceInfo> list = new CopyOnWriteArrayList<>(); long size = allBytes; long left = 0; long page = 0; while (size > 0) { long start = 0; long end; start = left; //分页 if (size < PER_PAGE) { end = left + size; } else { end = left += PER_PAGE; } size -= PER_PAGE; page++; if (start != 0) { start++; } log.info("页码:{},开始位置:{},结束位置:{}", page, start, end); final SliceInfo sliceInfo = new SliceInfo(start, end, page); list.add(sliceInfo); } SlicePageInfo slicePageInfo = new SlicePageInfo(); slicePageInfo.setSliceInfoList(list); slicePageInfo.setPage(page); return slicePageInfo; } }
@Test @DisplayName("大文件分片下载") public void downloadSliceFile() { DownLoadEngine.downloadSlice("https://dldir1.qq.com/qqfile/qq/PCQQ9.6.1/QQ9.6.1.28732.exe", "D:/temp", "qq.exe"); }