feat: 支持 Torrent 解析与数据持久化(v1/v2/Hybrid)
- 实现对 Torrent v1、v2 以及 Hybrid 模式的解析
- 完成 Torrent 元信息的数据库持久化逻辑
- 支持 announce 节点、文件信息的提取与存储
- 构建 BtTorrent 表结构与相关插入逻辑
- 预留 OSS 上传接口,明日补充实际上传实现
Change-Id: I4438da0ab364bc8e0d299e1d57474c190584052a
diff --git a/react-ui/src/pages/Torrent/index.tsx b/react-ui/src/pages/Torrent/index.tsx
index 6990da7..bb058ae 100644
--- a/react-ui/src/pages/Torrent/index.tsx
+++ b/react-ui/src/pages/Torrent/index.tsx
@@ -1,5 +1,5 @@
import React, { useRef, useState } from 'react';
-import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined } from '@ant-design/icons';
import {
Button,
Modal,
@@ -11,14 +11,10 @@
DatePicker,
Card,
Layout,
+ Upload,
+ UploadProps,
} from 'antd';
-import {
- ProTable,
- ActionType,
- ProColumns,
- ProDescriptions,
- ProDescriptionsItemProps,
-} from '@ant-design/pro-components';
+import { ProTable, ActionType, ProColumns, ProDescriptions, ProDescriptionsItemProps } from '@ant-design/pro-components';
import type { BtTorrent } from './data';
import {
listBtTorrent,
@@ -26,6 +22,7 @@
addBtTorrent,
updateBtTorrent,
removeBtTorrent,
+ uploadTorrent, // Function to handle torrent upload
} from './service';
const { Content } = Layout;
@@ -36,7 +33,10 @@
const [modalVisible, setModalVisible] = useState(false);
const [drawerVisible, setDrawerVisible] = useState(false);
const [current, setCurrent] = useState<Partial<BtTorrent>>({});
+ const [uploadModalVisible, setUploadModalVisible] = useState(false); // State for upload modal
+ const [uploadFile, setUploadFile] = useState<File | null>(null); // State to store selected file
+ // Columns for the ProTable (the table displaying torrents)
const columns: ProColumns<BtTorrent>[] = [
{
title: '种子ID',
@@ -89,18 +89,47 @@
},
];
+ // Handle the submit for adding or updating a torrent
const handleSubmit = async () => {
const values = await form.validateFields();
- if (current?.torrentId) {
- await updateBtTorrent({ ...current, ...values });
- message.success('更新成功');
- } else {
- await addBtTorrent(values as BtTorrent);
- message.success('新增成功');
+ try {
+ if (current?.torrentId) {
+ await updateBtTorrent({ ...current, ...values });
+ message.success('更新成功');
+ } else {
+ await addBtTorrent(values as BtTorrent);
+ message.success('新增成功');
+ }
+ setModalVisible(false);
+ form.resetFields();
+ actionRef.current?.reload();
+ } catch (error) {
+ message.error('操作失败');
}
- setModalVisible(false);
- form.resetFields();
- actionRef.current?.reload();
+ };
+
+ // Handle file upload
+ const handleFileUpload = async (file: File) => {
+ try {
+ if (!file) {
+ throw new Error('请选择一个文件');
+ }
+
+ // Call the uploadTorrent function to upload the file
+ await uploadTorrent(file);
+
+ // Show a success message
+ message.success('文件上传成功');
+
+ // Close the upload modal
+ setUploadModalVisible(false);
+
+ // Optionally reload the table or perform other actions (e.g., refresh list)
+ actionRef.current?.reload();
+
+ } catch (error) {
+ message.error(error.message || '文件上传失败');
+ }
};
return (
@@ -124,6 +153,14 @@
>
新增
</Button>,
+ <Button
+ key="upload"
+ type="primary"
+ icon={<UploadOutlined />}
+ onClick={() => setUploadModalVisible(true)} // Show the upload modal
+ >
+ 上传种子文件
+ </Button>
]}
request={async (params) => {
const res = await listBtTorrent(params);
@@ -162,6 +199,26 @@
</Form>
</Modal>
+ {/* 上传种子文件的Modal */}
+ <Modal
+ title="上传种子文件"
+ visible={uploadModalVisible}
+ onCancel={() => setUploadModalVisible(false)}
+ footer={null}
+ >
+ <Upload
+ customRequest={({ file, onSuccess, onError }) => {
+ setUploadFile(file);
+ handleFileUpload(file);
+ onSuccess?.();
+ }}
+ showUploadList={false}
+ accept=".torrent"
+ >
+ <Button icon={<UploadOutlined />}>点击上传 .torrent 文件</Button>
+ </Upload>
+ </Modal>
+
{/* 详情抽屉 */}
<Drawer
width={500}
diff --git a/react-ui/src/pages/Torrent/service.ts b/react-ui/src/pages/Torrent/service.ts
index c1d8e26..8e15618 100644
--- a/react-ui/src/pages/Torrent/service.ts
+++ b/react-ui/src/pages/Torrent/service.ts
@@ -166,7 +166,23 @@
data,
});
}
+/**
+ * 上传并解析种子文件
+ * @param file The .torrent file to upload
+ * @returns The parsed torrent data or error message
+ */
+export async function uploadTorrent(file: File) {
+ const formData = new FormData();
+ formData.append('file', file);
+ return request('/api/system/torrent/uploadTorrent', {
+ method: 'POST',
+ data: formData,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+}
/** 删除标签(支持批量) */
export async function removeBtTorrentTag(ids: number[]) {
return request(`/api/system/tags/${ids.join(',')}`, {
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index fa5532d..4fe1dc0 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -17,6 +17,16 @@
<dependencies>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
+
+
+
+
+
+
<!-- spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -24,6 +34,8 @@
<optional>true</optional> <!-- 表示依赖不会传递 -->
</dependency>
+
+
<!-- spring-doc -->
<dependency>
<groupId>org.springdoc</groupId>
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
index 2dab95a..6d7626b 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
@@ -1,7 +1,20 @@
package com.ruoyi.torrent.controller;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.torrent.domain.BtTorrentAnnounce;
+import com.ruoyi.torrent.domain.BtTorrentFile;
+import com.ruoyi.torrent.service.IBtTorrentAnnounceService;
+import com.ruoyi.torrent.service.IBtTorrentFileService;
import com.ruoyi.torrent.service.IBtTorrentService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -14,6 +27,9 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -22,6 +38,11 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.UUID;
+
/**
* 种子主Controller
*
@@ -35,6 +56,125 @@
@Autowired
private IBtTorrentService btTorrentService;
+ @Autowired
+ private IBtTorrentFileService btTorrentFileService;
+ @Autowired
+ private IBtTorrentAnnounceService btTorrentAnnounceService;
+
+ private static final String boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
+
+ @PreAuthorize("@ss.hasPermi('system:torrent:add')")
+ @Log(title = "种子主", businessType = BusinessType.INSERT)
+ @PostMapping("/uploadTorrent")
+ public AjaxResult uploadTorrent(@RequestParam("file") MultipartFile file) {
+ try {
+
+ // Create URL connection to Flask server
+ String flaskUrl = "http://localhost:5000/parse_torrent"; // Flask server URL
+ HttpURLConnection connection = (HttpURLConnection) new URL(flaskUrl).openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
+
+ // Open output stream
+ try (OutputStream outputStream = connection.getOutputStream()) {
+ // Write the multipart form data boundary and file data
+ writeFormData(outputStream, file);
+
+ // Handle Flask response
+ int responseCode = connection.getResponseCode();
+ if (responseCode != 200) {
+ return AjaxResult.error("Failed to communicate with Flask server, response code: " + responseCode);
+ }
+
+ // Assuming the Flask server responds with JSON, parse the response
+ String responseBody = new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+ parseTorrentData(responseBody);
+ return AjaxResult.success("Torrent uploaded and processed successfully");
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return AjaxResult.error("Error during file upload or communication with Flask service");
+ }
+ }
+
+ // Write multipart form data to the output stream
+ private void writeFormData(OutputStream outputStream, MultipartFile file) throws IOException {
+ // Write the start boundary
+ outputStream.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
+
+ // Write content-disposition header
+ outputStream.write(("Content-Disposition: form-data; name=\"torrent\"; filename=\"" + file.getOriginalFilename() + "\"\r\n").getBytes(StandardCharsets.UTF_8));
+ outputStream.write(("Content-Type: application/octet-stream\r\n\r\n").getBytes(StandardCharsets.UTF_8));
+
+ // Write the file content
+ file.getInputStream().transferTo(outputStream);
+ outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8));
+
+ // Write the closing boundary
+ outputStream.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+ }
+
+
+ public void parseTorrentData(String responseData) {
+ try {
+
+ // Create ObjectMapper to handle JSON parsing
+ ObjectMapper objectMapper = new ObjectMapper();
+
+ // Parse the JSON response
+ JsonNode jsonNode = objectMapper.readTree(responseData);
+ JsonNode btTorrentNode = jsonNode.get("btTorrent");
+ JsonNode btTorrentAnnounceNode = jsonNode.get("btTorrentAnnounce");
+ JsonNode btTorrentFilesNode = jsonNode.get("btTorrentFiles");
+
+
+ // Convert btTorrentNode to BtTorrent object
+ BtTorrent btTorrent = objectMapper.readValue(btTorrentNode.toString(), BtTorrent.class);
+ btTorrent.setCreatedBy(SecurityUtils.getUsername());
+ btTorrent.setUploaderId(SecurityUtils.getUserId());
+ btTorrentService.insertBtTorrent(btTorrent);
+ Long torrentId=btTorrent.getTorrentId();
+
+ // Convert btTorrentFilesNode to List<BtTorrentFile> using TypeReference
+ List<BtTorrentFile> btTorrentFiles = objectMapper.readValue(
+ btTorrentFilesNode.toString(),
+ new TypeReference<List<BtTorrentFile>>() {});
+ btTorrentFiles.forEach(btFile -> {
+ btFile.setTorrentId(torrentId);
+ btTorrentFileService.insertBtTorrentFile(btFile);
+ }
+ );
+
+ // Convert btTorrentAnnounceNode to List<BtTorrentAnnounce>
+ List<BtTorrentAnnounce> btTorrentAnnounceList = new ArrayList<>();
+ if (btTorrentAnnounceNode.isArray()) {
+ for (JsonNode announceNode : btTorrentAnnounceNode) {
+ // You may need to adjust this depending on your BtTorrentAnnounce class structure
+ BtTorrentAnnounce announce = objectMapper.readValue(announceNode.toString(), BtTorrentAnnounce.class);
+ btTorrentAnnounceList.add(announce);
+ }
+ }
+ btTorrentAnnounceList.forEach(btTorrentAnnounce -> {
+ btTorrentAnnounce.setTorrentId(torrentId);
+ btTorrentAnnounceService.insertBtTorrentAnnounce(btTorrentAnnounce);
+ }
+ );
+
+
+
+
+
+
+
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ // Handle the error (e.g., return null or an error message)
+
+ }
+ }
+
/**
* 查询种子主列表
*/
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java
index 9a85f2f..30fe780 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java
@@ -2,6 +2,7 @@
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@@ -13,6 +14,7 @@
* @author ruoyi
* @date 2025-04-21
*/
+@JsonIgnoreProperties(ignoreUnknown = true)
public class BtTorrent extends BaseEntity
{
private static final long serialVersionUID = 1L;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java
index 967cc5d..9736a4f 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java
@@ -1,5 +1,6 @@
package com.ruoyi.torrent.domain;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@@ -11,6 +12,7 @@
* @author ruoyi
* @date 2025-04-21
*/
+@JsonIgnoreProperties(ignoreUnknown = true)
public class BtTorrentAnnounce extends BaseEntity
{
private static final long serialVersionUID = 1L;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java
index 920d871..485b8dd 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java
@@ -1,5 +1,6 @@
package com.ruoyi.torrent.domain;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@@ -11,6 +12,7 @@
* @author ruoyi
* @date 2025-04-21
*/
+@JsonIgnoreProperties(ignoreUnknown = true)
public class BtTorrentFile extends BaseEntity
{
private static final long serialVersionUID = 1L;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java
index 8f19b72..ccb81ea 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java
@@ -1,5 +1,6 @@
package com.ruoyi.torrent.domain;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@@ -11,6 +12,7 @@
* @author ruoyi
* @date 2025-04-21
*/
+@JsonIgnoreProperties(ignoreUnknown = true)
public class BtTorrentTags extends BaseEntity
{
private static final long serialVersionUID = 1L;