feat:代码结构
parent
9a64e26caf
commit
ddc23832b7
|
@ -0,0 +1,14 @@
|
|||
# 使用官方OpenJDK作为基础镜像
|
||||
FROM openjdk:11-jre-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制项目的jar文件到容器中
|
||||
COPY target/*.jar app.jar
|
||||
|
||||
# 暴露应用端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>chunked-upload-demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>nex-be</name>
|
||||
<description>开箱即用管理端</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Configuration Processor (可选) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.dampcake</groupId>
|
||||
<artifactId>bencode</artifactId>
|
||||
<version>1.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
||||
<version>3.0.3</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,15 @@
|
|||
package com.unisinsight.project;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 分片上传应用启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.unisinsight.project.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.service.ApiInfo;
|
||||
import springfox.documentation.service.Contact;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
@Configuration
|
||||
@EnableSwagger2
|
||||
@Import(BeanValidatorPluginsConfiguration.class)
|
||||
@EnableWebMvc
|
||||
public class Knife4jConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public Docket defaultApi() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
.apiInfo(apiInfo())
|
||||
.groupName("默认分组")
|
||||
.select()
|
||||
.apis(RequestHandlerSelectors.basePackage("org.hz.controller")) // 修改为你的controller包路径
|
||||
.paths(PathSelectors.any())
|
||||
.build();
|
||||
}
|
||||
|
||||
private ApiInfo apiInfo() {
|
||||
return new ApiInfoBuilder()
|
||||
.title("文件分片上传服务API")
|
||||
.description("# 大文件分片上传接口文档\n\n"
|
||||
+ "## 功能说明\n"
|
||||
+ "1. 支持大文件分片上传\n"
|
||||
+ "2. 支持断点续传\n"
|
||||
+ "3. 自动合并完整文件\n\n"
|
||||
+ "## 使用流程\n"
|
||||
+ "1. 前端将大文件分片\n"
|
||||
+ "2. 依次上传各分片\n"
|
||||
+ "3. 系统自动合并文件")
|
||||
.termsOfServiceUrl("http://localhost:8080/")
|
||||
.contact(new Contact("API开发者", "https://example.com", "developer@example.com"))
|
||||
.version("1.0.0")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 配置Swagger UI和WebJars资源的访问路径,使得这些静态资源可以通过特定的URL路径在Web应用中被访问
|
||||
registry.addResourceHandler("doc.html")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
|
||||
registry.addResourceHandler("swagger-ui.html")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
registry.addResourceHandler("/webjars/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
package com.unisinsight.project.controller;
|
||||
|
||||
import io.swagger.annotations.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 大文件分片上传控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
@Api(tags = "文件分片上传接口")
|
||||
public class FileChunkController {
|
||||
|
||||
// 临时目录,用于存储上传的分片
|
||||
@Value("${file.upload.temp-dir:${java.io.tmpdir}/chunked-uploads}")
|
||||
private String tempDir;
|
||||
|
||||
// 最终文件存储目录
|
||||
@Value("${file.upload.dir:${user.home}/uploads}")
|
||||
private String uploadDir;
|
||||
|
||||
// 存储每个文件的分片信息
|
||||
private final Map<String, FileUploadInfo> fileUploadMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 上传文件分片
|
||||
*
|
||||
* @param chunk 分片文件
|
||||
* @param fileId 文件唯一标识符
|
||||
* @param chunkNumber 当前分片编号(从1开始)
|
||||
* @param totalChunks 总分片数
|
||||
* @param fileName 原始文件名
|
||||
* @param totalSize 文件总大小
|
||||
* @return 上传结果
|
||||
*/
|
||||
@PostMapping("/upload-chunk")
|
||||
@ApiOperation(value = "上传文件分片", notes = "上传单个文件分片,当所有分片上传完成后自动合并文件")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "chunk", value = "文件分片", required = true, dataType = "__File", paramType = "form"),
|
||||
@ApiImplicitParam(name = "fileId", value = "文件唯一标识符", required = true, dataType = "String", paramType = "query"),
|
||||
@ApiImplicitParam(name = "chunkNumber", value = "当前分片编号(从1开始)", required = true, dataType = "int", paramType = "query"),
|
||||
@ApiImplicitParam(name = "totalChunks", value = "总分片数", required = true, dataType = "int", paramType = "query"),
|
||||
@ApiImplicitParam(name = "fileName", value = "原始文件名", required = true, dataType = "String", paramType = "query"),
|
||||
@ApiImplicitParam(name = "totalSize", value = "文件总大小", required = true, dataType = "long", paramType = "query")
|
||||
})
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "上传成功"),
|
||||
@ApiResponse(code = 500, message = "服务器内部错误")
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> uploadChunk(
|
||||
@RequestParam("chunk") MultipartFile chunk,
|
||||
@RequestParam("fileId") String fileId,
|
||||
@RequestParam("chunkNumber") int chunkNumber,
|
||||
@RequestParam("totalChunks") int totalChunks,
|
||||
@RequestParam("fileName") String fileName,
|
||||
@RequestParam("totalSize") long totalSize) {
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 创建临时目录
|
||||
Path fileTempDir = Paths.get(tempDir, fileId);
|
||||
if (!Files.exists(fileTempDir)) {
|
||||
Files.createDirectories(fileTempDir);
|
||||
}
|
||||
|
||||
// 保存分片文件
|
||||
String chunkFileName = String.format("%05d.part", chunkNumber);
|
||||
Path chunkFilePath = fileTempDir.resolve(chunkFileName);
|
||||
chunk.transferTo(chunkFilePath);
|
||||
|
||||
// 更新文件上传信息
|
||||
FileUploadInfo uploadInfo = fileUploadMap.computeIfAbsent(fileId,
|
||||
id -> new FileUploadInfo(id, fileName, totalChunks, totalSize));
|
||||
uploadInfo.addUploadedChunk(chunkNumber);
|
||||
|
||||
// 检查是否所有分片都已上传
|
||||
if (uploadInfo.isUploadComplete()) {
|
||||
// 合并文件
|
||||
Path finalDir = Paths.get(uploadDir);
|
||||
if (!Files.exists(finalDir)) {
|
||||
Files.createDirectories(finalDir);
|
||||
}
|
||||
|
||||
Path finalFilePath = finalDir.resolve(fileName);
|
||||
mergeChunks(fileId, finalFilePath, totalChunks);
|
||||
|
||||
// 清理临时文件
|
||||
cleanupTempFiles(fileId);
|
||||
|
||||
// 从上传映射中移除
|
||||
fileUploadMap.remove(fileId);
|
||||
|
||||
response.put("status", "completed");
|
||||
response.put("message", "文件上传并合并完成");
|
||||
response.put("filePath", finalFilePath.toString());
|
||||
} else {
|
||||
response.put("status", "uploading");
|
||||
response.put("message", "分片上传成功");
|
||||
response.put("uploadedChunks", uploadInfo.getUploadedChunks().size());
|
||||
response.put("totalChunks", totalChunks);
|
||||
}
|
||||
|
||||
response.put("success", true);
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
response.put("success", false);
|
||||
response.put("message", "上传失败: " + e.getMessage());
|
||||
return ResponseEntity.status(500).body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询文件上传状态
|
||||
*
|
||||
* @param fileId 文件唯一标识符
|
||||
* @return 上传状态信息
|
||||
*/
|
||||
@GetMapping("/upload-status/{fileId}")
|
||||
@ApiOperation("查询文件上传状态")
|
||||
public ResponseEntity<Map<String, Object>> getUploadStatus(@PathVariable String fileId) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
FileUploadInfo uploadInfo = fileUploadMap.get(fileId);
|
||||
if (uploadInfo == null) {
|
||||
// 检查文件是否已经完成上传并合并
|
||||
try {
|
||||
Path finalFilePath = Paths.get(uploadDir, fileId);
|
||||
if (Files.exists(finalFilePath)) {
|
||||
response.put("status", "completed");
|
||||
response.put("message", "文件上传已完成");
|
||||
response.put("filePath", finalFilePath.toString());
|
||||
} else {
|
||||
response.put("status", "not_found");
|
||||
response.put("message", "文件上传信息不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
response.put("status", "error");
|
||||
response.put("message", "查询状态失败: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
response.put("status", "uploading");
|
||||
response.put("uploadedChunks", uploadInfo.getUploadedChunks().size());
|
||||
response.put("totalChunks", uploadInfo.getTotalChunks());
|
||||
response.put("progress", (double) uploadInfo.getUploadedChunks().size() / uploadInfo.getTotalChunks());
|
||||
}
|
||||
|
||||
response.put("success", true);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并所有分片文件
|
||||
*
|
||||
* @param fileId 文件唯一标识符
|
||||
* @param outputPath 合并后的文件路径
|
||||
* @param totalChunks 总分片数
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
private void mergeChunks(String fileId, Path outputPath, int totalChunks) throws IOException {
|
||||
try (OutputStream outputStream = Files.newOutputStream(outputPath)) {
|
||||
Path fileTempDir = Paths.get(tempDir, fileId);
|
||||
|
||||
// 按顺序合并分片
|
||||
for (int i = 1; i <= totalChunks; i++) {
|
||||
String chunkFileName = String.format("%05d.part", i);
|
||||
Path chunkPath = fileTempDir.resolve(chunkFileName);
|
||||
|
||||
if (!Files.exists(chunkPath)) {
|
||||
throw new IOException("缺少分片文件: " + chunkFileName);
|
||||
}
|
||||
|
||||
// 将分片内容追加到输出文件
|
||||
Files.copy(chunkPath, outputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时分片文件
|
||||
*
|
||||
* @param fileId 文件唯一标识符
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
private void cleanupTempFiles(String fileId) throws IOException {
|
||||
Path fileTempDir = Paths.get(tempDir, fileId);
|
||||
if (Files.exists(fileTempDir)) {
|
||||
// 递归删除临时目录及其内容
|
||||
Files.walk(fileTempDir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传信息类
|
||||
*/
|
||||
private static class FileUploadInfo {
|
||||
private final String fileId;
|
||||
private final String fileName;
|
||||
private final int totalChunks;
|
||||
private final long totalSize;
|
||||
private final Set<Integer> uploadedChunks;
|
||||
|
||||
public FileUploadInfo(String fileId, String fileName, int totalChunks, long totalSize) {
|
||||
this.fileId = fileId;
|
||||
this.fileName = fileName;
|
||||
this.totalChunks = totalChunks;
|
||||
this.totalSize = totalSize;
|
||||
this.uploadedChunks = ConcurrentHashMap.newKeySet();
|
||||
}
|
||||
|
||||
public void addUploadedChunk(int chunkNumber) {
|
||||
uploadedChunks.add(chunkNumber);
|
||||
}
|
||||
|
||||
public boolean isUploadComplete() {
|
||||
return uploadedChunks.size() == totalChunks;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public int getTotalChunks() {
|
||||
return totalChunks;
|
||||
}
|
||||
|
||||
public long getTotalSize() {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public Set<Integer> getUploadedChunks() {
|
||||
return uploadedChunks;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/test")
|
||||
@ApiOperation("测试")
|
||||
public ResponseEntity<String> getUploadStatus() {
|
||||
return ResponseEntity.ok("ok");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
package com.unisinsight.project.util;
|
||||
|
||||
import com.dampcake.bencode.BencodeInputStream;
|
||||
import com.dampcake.bencode.BencodeOutputStream;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* BT种子生成和下载工具类
|
||||
*/
|
||||
public class BtTorrentUtil {
|
||||
|
||||
/**
|
||||
* 创建BT种子文件
|
||||
*
|
||||
* @param filePath 要分享的文件或目录路径
|
||||
* @param trackerUrl Tracker服务器地址
|
||||
* @param outputFile 生成的种子文件路径
|
||||
* @throws IOException IO异常
|
||||
* @throws URISyntaxException URI语法异常
|
||||
*/
|
||||
public static void createTorrent(String filePath, String trackerUrl, String outputFile)
|
||||
throws IOException, URISyntaxException {
|
||||
|
||||
Path path = Paths.get(filePath);
|
||||
if (!Files.exists(path)) {
|
||||
throw new FileNotFoundException("文件或目录不存在: " + filePath);
|
||||
}
|
||||
|
||||
// 验证tracker URL格式
|
||||
new URI(trackerUrl);
|
||||
|
||||
// 创建种子信息
|
||||
TorrentInfo torrentInfo = new TorrentInfo();
|
||||
torrentInfo.setAnnounce(trackerUrl);
|
||||
torrentInfo.setCreationDate(new Date().getTime() / 1000);
|
||||
torrentInfo.setCreatedBy("BtTorrentUtil v1.0");
|
||||
|
||||
// 处理文件信息
|
||||
if (Files.isDirectory(path)) {
|
||||
processDirectory(path, torrentInfo);
|
||||
} else {
|
||||
processFile(path, torrentInfo);
|
||||
}
|
||||
|
||||
// 生成种子文件
|
||||
writeTorrentFile(torrentInfo, outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个文件
|
||||
*/
|
||||
private static void processFile(Path filePath, TorrentInfo torrentInfo) throws IOException {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setLength(Files.size(filePath));
|
||||
fileInfo.setPath(new ArrayList<>());
|
||||
fileInfo.getPath().add(filePath.getFileName().toString());
|
||||
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
files.add(fileInfo);
|
||||
|
||||
torrentInfo.setFiles(files);
|
||||
torrentInfo.setName(filePath.getFileName().toString());
|
||||
torrentInfo.setPieceLength(524288); // 512KB
|
||||
|
||||
// 计算文件的pieces
|
||||
byte[] pieces = calculatePieces(filePath, torrentInfo.getPieceLength());
|
||||
torrentInfo.setPieces(pieces);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理目录
|
||||
*/
|
||||
private static void processDirectory(Path dirPath, TorrentInfo torrentInfo) throws IOException {
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
long totalSize = 0;
|
||||
|
||||
Files.walk(dirPath)
|
||||
.filter(Files::isRegularFile)
|
||||
.forEach(file -> {
|
||||
try {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setLength(Files.size(file));
|
||||
List<String> pathList = new ArrayList<>();
|
||||
|
||||
// 获取相对路径
|
||||
Path relativePath = dirPath.relativize(file);
|
||||
for (Path part : relativePath) {
|
||||
pathList.add(part.toString());
|
||||
}
|
||||
|
||||
fileInfo.setPath(pathList);
|
||||
files.add(fileInfo);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
torrentInfo.setFiles(files);
|
||||
torrentInfo.setName(dirPath.getFileName().toString());
|
||||
torrentInfo.setPieceLength(524288); // 512KB
|
||||
|
||||
// 计算所有文件的pieces(简化实现)
|
||||
// 实际应用中需要按顺序读取所有文件数据来计算pieces
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
for (FileInfo file : files) {
|
||||
Path filePath = dirPath;
|
||||
for (String pathPart : file.getPath()) {
|
||||
filePath = filePath.resolve(pathPart);
|
||||
}
|
||||
if (Files.exists(filePath)) {
|
||||
Files.copy(filePath, baos);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] pieces = calculatePiecesFromData(baos.toByteArray(), torrentInfo.getPieceLength());
|
||||
torrentInfo.setPieces(pieces);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文件的pieces哈希值
|
||||
*/
|
||||
private static byte[] calculatePieces(Path filePath, int pieceLength) throws IOException {
|
||||
ByteArrayOutputStream pieces = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[pieceLength];
|
||||
int bytesRead;
|
||||
|
||||
try (InputStream is = Files.newInputStream(filePath)) {
|
||||
while ((bytesRead = is.read(buffer)) != -1) {
|
||||
// 简化实现,实际应使用SHA1哈希
|
||||
// 这里只是示例,实际应用中需要计算SHA1
|
||||
byte[] hash = new byte[20]; // SHA1哈希长度为20字节
|
||||
for (int i = 0; i < Math.min(20, bytesRead); i++) {
|
||||
hash[i] = (byte) (buffer[i] % 256);
|
||||
}
|
||||
pieces.write(hash);
|
||||
}
|
||||
}
|
||||
|
||||
return pieces.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据计算pieces哈希值
|
||||
*/
|
||||
private static byte[] calculatePiecesFromData(byte[] data, int pieceLength) throws IOException {
|
||||
ByteArrayOutputStream pieces = new ByteArrayOutputStream();
|
||||
|
||||
for (int i = 0; i < data.length; i += pieceLength) {
|
||||
int length = Math.min(pieceLength, data.length - i);
|
||||
byte[] piece = new byte[length];
|
||||
System.arraycopy(data, i, piece, 0, length);
|
||||
|
||||
// 简化实现,实际应使用SHA1哈希
|
||||
byte[] hash = new byte[20];
|
||||
for (int j = 0; j < Math.min(20, length); j++) {
|
||||
hash[j] = (byte) (piece[j] % 256);
|
||||
}
|
||||
pieces.write(hash);
|
||||
}
|
||||
|
||||
return pieces.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入种子文件
|
||||
*/
|
||||
private static void writeTorrentFile(TorrentInfo torrentInfo, String outputFile) throws IOException {
|
||||
try (FileOutputStream fos = new FileOutputStream(outputFile);
|
||||
BencodeOutputStream bos = new BencodeOutputStream(fos)) {
|
||||
|
||||
// 构建种子数据结构
|
||||
java.util.Map<String, Object> torrentMap = new java.util.HashMap<>();
|
||||
torrentMap.put("announce", torrentInfo.getAnnounce());
|
||||
torrentMap.put("creation date", torrentInfo.getCreationDate());
|
||||
torrentMap.put("created by", torrentInfo.getCreatedBy());
|
||||
|
||||
// info字典
|
||||
java.util.Map<String, Object> infoMap = new java.util.HashMap<>();
|
||||
infoMap.put("name", torrentInfo.getName());
|
||||
infoMap.put("piece length", torrentInfo.getPieceLength());
|
||||
infoMap.put("pieces", torrentInfo.getPieces());
|
||||
|
||||
if (torrentInfo.getFiles() != null && !torrentInfo.getFiles().isEmpty()) {
|
||||
// 多文件模式
|
||||
java.util.List<java.util.Map<String, Object>> fileList = new ArrayList<>();
|
||||
for (FileInfo file : torrentInfo.getFiles()) {
|
||||
java.util.Map<String, Object> fileMap = new java.util.HashMap<>();
|
||||
fileMap.put("length", file.getLength());
|
||||
fileMap.put("path", file.getPath());
|
||||
fileList.add(fileMap);
|
||||
}
|
||||
infoMap.put("files", fileList);
|
||||
} else {
|
||||
// 单文件模式
|
||||
infoMap.put("length", torrentInfo.getLength());
|
||||
}
|
||||
|
||||
torrentMap.put("info", infoMap);
|
||||
|
||||
// 写入文件
|
||||
bos.writeDictionary(torrentMap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供种子文件下载
|
||||
*
|
||||
* @param torrentFilePath 种子文件路径
|
||||
* @param downloadPath 下载保存路径
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
public static void downloadTorrent(String torrentFilePath, String downloadPath) throws IOException {
|
||||
Path source = Paths.get(torrentFilePath);
|
||||
Path target = Paths.get(downloadPath);
|
||||
|
||||
if (!Files.exists(source)) {
|
||||
throw new FileNotFoundException("种子文件不存在: " + torrentFilePath);
|
||||
}
|
||||
|
||||
Files.copy(source, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析种子文件信息
|
||||
*
|
||||
* @param torrentFilePath 种子文件路径
|
||||
* @return 种子信息
|
||||
* @throws IOException IO异常
|
||||
*/
|
||||
public static TorrentInfo parseTorrent(String torrentFilePath) throws IOException {
|
||||
try (FileInputStream fis = new FileInputStream(torrentFilePath);
|
||||
BencodeInputStream bis = new BencodeInputStream(fis)) {
|
||||
|
||||
java.util.Map<String, Object> torrentMap = bis.readDictionary();
|
||||
TorrentInfo torrentInfo = new TorrentInfo();
|
||||
|
||||
torrentInfo.setAnnounce((String) torrentMap.get("announce"));
|
||||
|
||||
Object creationDate = torrentMap.get("creation date");
|
||||
if (creationDate instanceof Number) {
|
||||
torrentInfo.setCreationDate(((Number) creationDate).longValue());
|
||||
}
|
||||
|
||||
torrentInfo.setCreatedBy((String) torrentMap.get("created by"));
|
||||
|
||||
// 解析info部分
|
||||
java.util.Map<String, Object> infoMap = (java.util.Map<String, Object>) torrentMap.get("info");
|
||||
if (infoMap != null) {
|
||||
torrentInfo.setName((String) infoMap.get("name"));
|
||||
|
||||
Object pieceLength = infoMap.get("piece length");
|
||||
if (pieceLength instanceof Number) {
|
||||
torrentInfo.setPieceLength(((Number) pieceLength).intValue());
|
||||
}
|
||||
|
||||
torrentInfo.setPieces((byte[]) infoMap.get("pieces"));
|
||||
|
||||
Object length = infoMap.get("length");
|
||||
if (length instanceof Number) {
|
||||
torrentInfo.setLength(((Number) length).longValue());
|
||||
}
|
||||
|
||||
// 处理文件列表
|
||||
Object filesObj = infoMap.get("files");
|
||||
if (filesObj instanceof java.util.List) {
|
||||
java.util.List<java.util.Map<String, Object>> fileList =
|
||||
(java.util.List<java.util.Map<String, Object>>) filesObj;
|
||||
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
for (java.util.Map<String, Object> fileMap : fileList) {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
|
||||
Object fileLength = fileMap.get("length");
|
||||
if (fileLength instanceof Number) {
|
||||
fileInfo.setLength(((Number) fileLength).longValue());
|
||||
}
|
||||
|
||||
fileInfo.setPath((List<String>) fileMap.get("path"));
|
||||
files.add(fileInfo);
|
||||
}
|
||||
torrentInfo.setFiles(files);
|
||||
}
|
||||
}
|
||||
|
||||
return torrentInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 种子信息类
|
||||
*/
|
||||
public static class TorrentInfo {
|
||||
private String announce;
|
||||
private long creationDate;
|
||||
private String createdBy;
|
||||
private String name;
|
||||
private long length;
|
||||
private int pieceLength;
|
||||
private byte[] pieces;
|
||||
private List<FileInfo> files;
|
||||
|
||||
// Getters and Setters
|
||||
public String getAnnounce() {
|
||||
return announce;
|
||||
}
|
||||
|
||||
public void setAnnounce(String announce) {
|
||||
this.announce = announce;
|
||||
}
|
||||
|
||||
public long getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(long creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(long length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public int getPieceLength() {
|
||||
return pieceLength;
|
||||
}
|
||||
|
||||
public void setPieceLength(int pieceLength) {
|
||||
this.pieceLength = pieceLength;
|
||||
}
|
||||
|
||||
public byte[] getPieces() {
|
||||
return pieces;
|
||||
}
|
||||
|
||||
public void setPieces(byte[] pieces) {
|
||||
this.pieces = pieces;
|
||||
}
|
||||
|
||||
public List<FileInfo> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public void setFiles(List<FileInfo> files) {
|
||||
this.files = files;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件信息类
|
||||
*/
|
||||
public static class FileInfo {
|
||||
private long length;
|
||||
private List<String> path;
|
||||
|
||||
// Getters and Setters
|
||||
public long getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(long length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public List<String> getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(List<String> path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
// 创建种子文件
|
||||
BtTorrentUtil.createTorrent(
|
||||
"/path/to/your/file_or_directory",
|
||||
"http://tracker.example.com:6969/announce",
|
||||
"/path/to/output.torrent"
|
||||
);
|
||||
|
||||
// 解析种子文件
|
||||
BtTorrentUtil.TorrentInfo info = BtTorrentUtil.parseTorrent("/path/to/output.torrent");
|
||||
System.out.println("种子名称: " + info.getName());
|
||||
System.out.println("创建时间: " + new Date(info.getCreationDate() * 1000));
|
||||
|
||||
// 下载种子文件
|
||||
BtTorrentUtil.downloadTorrent(
|
||||
"/path/to/output.torrent",
|
||||
"/path/to/download/location.torrent"
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# application.yml
|
||||
server:
|
||||
port: 8112
|
||||
file:
|
||||
upload:
|
||||
temp-dir: /tmp/chunked-uploads
|
||||
dir: /uploads
|
||||
|
||||
spring:
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
|
||||
knife4j:
|
||||
production: false
|
||||
basic:
|
||||
enable: false
|
Loading…
Reference in New Issue