add torrent upload(finished) and tracker announce(unimplemnt)
Change-Id: I017c03df2bc1c40c4a080837821d56dfe58d6eb6
diff --git a/src/main/java/com/example/g8backend/controller/AuthController.java b/src/main/java/com/example/g8backend/controller/AuthController.java
index 4b3be4b..2f36500 100644
--- a/src/main/java/com/example/g8backend/controller/AuthController.java
+++ b/src/main/java/com/example/g8backend/controller/AuthController.java
@@ -63,6 +63,9 @@
user.setUserName(registerDTO.getUserName());
user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
user.setEmail(registerDTO.getEmail());
+
+ // passkey 用于在客户端发送announce请求时获取用户信息
+ user.setPasskey(UUID.randomUUID().toString().replace("-", ""));
userService.registerUser(user);
return ResponseEntity.ok("注册成功");
diff --git a/src/main/java/com/example/g8backend/controller/TorrentController.java b/src/main/java/com/example/g8backend/controller/TorrentController.java
new file mode 100644
index 0000000..bec187e
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/TorrentController.java
@@ -0,0 +1,46 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.service.IUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.example.g8backend.service.ITorrentService;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+
+@RestController
+@RequestMapping("/torrent")
+public class TorrentController {
+ @Autowired
+ private ITorrentService torrentService;
+
+ @Autowired
+ private IUserService userService;
+
+ @RequestMapping("/upload")
+ public ResponseEntity<?> handleTorrentUpload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ long userId = (long) authentication.getPrincipal();
+
+ User user = userService.getById(userId);
+ String passkey = user.getPasskey();
+
+ File tempFile = File.createTempFile("upload-", ".torrent");
+ multipartFile.transferTo(tempFile);
+
+ torrentService.handleTorrentUpload(tempFile, userId, passkey);
+
+ // 删除临时文件
+ if(!tempFile.delete()){
+ throw new IOException("Failed to delete temporary file: " + tempFile.getAbsolutePath());
+ }
+ return ResponseEntity.ok("种子上传成功");
+ }
+}
diff --git a/src/main/java/com/example/g8backend/controller/TrackerController.java b/src/main/java/com/example/g8backend/controller/TrackerController.java
index 7ee9e5a..d6cc957 100644
--- a/src/main/java/com/example/g8backend/controller/TrackerController.java
+++ b/src/main/java/com/example/g8backend/controller/TrackerController.java
@@ -1,15 +1,33 @@
package com.example.g8backend.controller;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import com.example.g8backend.service.ITrackerService;
@RestController
-@RequestMapping("/announce")
+@RequestMapping("/tracker")
public class TrackerController {
+ @Autowired
+ private ITrackerService trackerService;
+
+ @Autowired
+ private RedisTemplate<String, Object> redisTemplate;
+
+ @GetMapping("/announce/{passkey}}")
+ public ResponseEntity<?> getAnnouncements(
+ @RequestParam(name = "info_hash") String infoHash,
+ @RequestParam(name = "peer_id") String peerId,
+ @RequestParam(name = "port") int port,
+ @RequestParam(name = "uploaded") long uploaded,
+ @RequestParam(name = "downloaded") long downloaded,
+ @RequestParam(name = "left") long left,
+ @RequestParam(name = "compact", required = false) int compact,
+ @RequestParam(name = "event", required = false) String event,
+ @PathVariable String passkey) {
+
+ return null;
+ }
}
diff --git a/src/main/java/com/example/g8backend/entity/Peer.java b/src/main/java/com/example/g8backend/entity/Peer.java
index 43302ce..b59f7ee 100644
--- a/src/main/java/com/example/g8backend/entity/Peer.java
+++ b/src/main/java/com/example/g8backend/entity/Peer.java
@@ -1,7 +1,5 @@
package com.example.g8backend.entity;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -10,7 +8,7 @@
public class Peer {
private Long peerId;
private String info_hash;
- private Long userId; // passkey from announce
+ private String passkey;
private String ipAddress;
private Integer port;
private Double uploaded;
diff --git a/src/main/java/com/example/g8backend/entity/Torrent.java b/src/main/java/com/example/g8backend/entity/Torrent.java
index b1e1985..cb9a4eb 100644
--- a/src/main/java/com/example/g8backend/entity/Torrent.java
+++ b/src/main/java/com/example/g8backend/entity/Torrent.java
@@ -12,15 +12,12 @@
@TableName("torrents")
public class Torrent {
@TableId(type = IdType.AUTO)
- private Integer torrentId;
+ private Long torrentId;
private Long userId;
private String torrentName;
private String infoHash;
private Double fileSize;
- @TableField("created_at")
- private Timestamp createdAt;
-
@Override
public String toString() {
return "Torrent{" +
@@ -29,7 +26,6 @@
", torrentName='" + torrentName + '\'' +
", infoHash='" + infoHash + '\'' +
", fileSize=" + fileSize +
- ", createdAt=" + createdAt +
'}';
}
}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/User.java b/src/main/java/com/example/g8backend/entity/User.java
index 8e28628..7ba8e03 100644
--- a/src/main/java/com/example/g8backend/entity/User.java
+++ b/src/main/java/com/example/g8backend/entity/User.java
@@ -11,6 +11,7 @@
@TableId(type = IdType.AUTO)
private Long userId;
+ private String passkey;
private String password;
private String userName;
private String email;
diff --git a/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java
index d251424..42c7e5b 100644
--- a/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java
+++ b/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java
@@ -27,7 +27,7 @@
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String path = request.getServletPath();
- if (path.startsWith("/auth")) {
+ if (path.startsWith("/auth") || path.startsWith("/tracker")) {
filterChain.doFilter(request, response);
return;
}
diff --git a/src/main/java/com/example/g8backend/mapper/PeerMapper.java b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
new file mode 100644
index 0000000..ce84bbe
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
@@ -0,0 +1,8 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Peer;
+
+public interface PeerMapper extends BaseMapper<Peer> {
+
+}
diff --git a/src/main/java/com/example/g8backend/mapper/TorrentMapper.java b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
new file mode 100644
index 0000000..f78a68b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Torrent;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+@Mapper
+public interface TorrentMapper extends BaseMapper<Torrent> {
+ int insertTorrent (@Param("userId") Long userId,
+ @Param("torrentName") String torrentName,
+ @Param("infoHash") String infoHash,
+ @Param("fileSize") Double fileSize);
+ Torrent getTorrentByInfoHash (@Param("infoHash") String infoHash);
+ Torrent getTorrentByTorrentId (@Param("torrentId") Long torrentId);
+}
diff --git a/src/main/java/com/example/g8backend/mapper/UserMapper.java b/src/main/java/com/example/g8backend/mapper/UserMapper.java
index b389eb7..0a9e562 100644
--- a/src/main/java/com/example/g8backend/mapper/UserMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/UserMapper.java
@@ -9,4 +9,5 @@
public interface UserMapper extends BaseMapper<User> {
User getUserByName(@Param("userName") String userName);
User getUserByEmail(@Param("email") String email);
+ User getUserByPasskey(@Param("passkey") String passkey);
}
diff --git a/src/main/java/com/example/g8backend/service/ITorrentService.java b/src/main/java/com/example/g8backend/service/ITorrentService.java
new file mode 100644
index 0000000..5e48dc4
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ITorrentService.java
@@ -0,0 +1,13 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.g8backend.entity.Torrent;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface ITorrentService extends IService<Torrent> {
+ Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException;
+ Torrent findByInfoHash(String infoHash);
+ Torrent findByTorrentId(Long torrentId);
+}
diff --git a/src/main/java/com/example/g8backend/service/IUserService.java b/src/main/java/com/example/g8backend/service/IUserService.java
index 2fb4fc9..f407c37 100644
--- a/src/main/java/com/example/g8backend/service/IUserService.java
+++ b/src/main/java/com/example/g8backend/service/IUserService.java
@@ -7,5 +7,6 @@
public interface IUserService extends IService<User> {
User getUserByName(@Param("name") String name);
User getUserByEmail(@Param("email") String email);
+ User getUserByPasskey(@Param("passkey") String passkey);
void registerUser(User user);;
}
diff --git a/src/main/java/com/example/g8backend/service/TrackerServiceImpl.java b/src/main/java/com/example/g8backend/service/TrackerServiceImpl.java
deleted file mode 100644
index ae12f70..0000000
--- a/src/main/java/com/example/g8backend/service/TrackerServiceImpl.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.example.g8backend.service;
-
-import org.springframework.stereotype.Service;
-
-@Service
-public class TrackerServiceImpl implements ITrackerService {
-}
diff --git a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
new file mode 100644
index 0000000..03185ba
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
@@ -0,0 +1,67 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.Torrent;
+import com.example.g8backend.mapper.TorrentMapper;
+import com.example.g8backend.service.ITorrentService;
+import com.example.g8backend.util.TorrentUtil;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+@Service
+public class TorrentServiceImpl extends ServiceImpl<TorrentMapper, Torrent> implements ITorrentService {
+ @Resource
+ private TorrentMapper torrentMapper;
+
+ @Override
+ public Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException{
+ String tracker = "http://127.0.0.1:8080/announce/" + passkey;
+
+ // 修改 announce 字段
+ byte[] modifiedBytes = TorrentUtil.injectTracker(file, tracker);
+
+ // 计算 info_hash
+ String infoHash = TorrentUtil.getInfoHash(file);
+
+ // 文件大小(以MB为单位)
+ double fileSize = file.length() / 1024.0 / 1024.0;
+
+ // 保存新的种子文件(可选)
+ File outputDir = new File("uploaded-torrents");
+ if (!outputDir.exists()) {
+ if (!outputDir.mkdirs()){
+ throw new IOException("Failed to create directory: " + outputDir.getAbsolutePath());
+ }
+ }
+
+ File savedFile = new File(outputDir, file.getName());
+ try (FileOutputStream fos = new FileOutputStream(savedFile)) {
+ fos.write(modifiedBytes);
+ }
+
+ // 插入数据库
+ torrentMapper.insertTorrent(userId, file.getName(), infoHash, fileSize);
+
+ // 构建返回实体
+ Torrent torrent = new Torrent();
+ torrent.setUserId(userId);
+ torrent.setTorrentName(file.getName());
+ torrent.setInfoHash(infoHash);
+ torrent.setFileSize(fileSize);
+ return torrent;
+ }
+
+ @Override
+ public Torrent findByInfoHash(String infoHash){
+ return torrentMapper.getTorrentByInfoHash(infoHash);
+ }
+
+ @Override
+ public Torrent findByTorrentId(Long torrentId){
+ return torrentMapper.getTorrentByTorrentId(torrentId);
+ }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
new file mode 100644
index 0000000..c0dba70
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
@@ -0,0 +1,8 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.service.ITrackerService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TrackerServiceImpl implements ITrackerService {
+}
diff --git a/src/main/java/com/example/g8backend/service/UserServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
similarity index 76%
rename from src/main/java/com/example/g8backend/service/UserServiceImpl.java
rename to src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
index 567c5f3..3f3357c 100644
--- a/src/main/java/com/example/g8backend/service/UserServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
@@ -1,11 +1,10 @@
-package com.example.g8backend.service;
+package com.example.g8backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.g8backend.entity.User;
import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IUserService;
import jakarta.annotation.Resource;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -14,9 +13,6 @@
@Resource
private UserMapper userMapper; // 手动注入 UserMapper
- @Autowired
- private PasswordEncoder passwordEncoder;
-
@Override
public User getUserByName(String name) { return userMapper.getUserByName(name);} // 调用 UserMapper 的自定义 SQL
@@ -24,5 +20,8 @@
public User getUserByEmail(String email) { return userMapper.getUserByEmail(email);}
@Override
+ public User getUserByPasskey(String passkey) { return userMapper.getUserByPasskey(passkey);}
+
+ @Override
public void registerUser(User user) {userMapper.insert(user);}
}
diff --git a/src/main/java/com/example/g8backend/util/TorrentUtil.java b/src/main/java/com/example/g8backend/util/TorrentUtil.java
index 37ce7da..f644fff 100644
--- a/src/main/java/com/example/g8backend/util/TorrentUtil.java
+++ b/src/main/java/com/example/g8backend/util/TorrentUtil.java
@@ -1,4 +1,66 @@
package com.example.g8backend.util;
+import com.dampcake.bencode.Bencode;
+import com.dampcake.bencode.Type;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.util.Map;
+
public class TorrentUtil {
+
+ private static final Bencode bencode = new Bencode();
+
+ public static byte[] injectTracker(File torrentFile, String trackerUrl) throws IOException {
+ byte[] fileBytes = readBytes(torrentFile);
+
+ Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
+
+ // trackerUrl: ip:port + /announce / {passkey}
+ torrentMap.put("announce", trackerUrl);
+
+ return bencode.encode(torrentMap);
+ }
+
+ public static String getInfoHash(File torrentFile) throws IOException {
+ byte[] fileBytes = readBytes(torrentFile);
+ Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> info = (Map<String, Object>) torrentMap.get("info");
+
+ // 对 info 字典重新编码
+ byte[] infoBytes = bencode.encode(info);
+
+ // 计算 SHA-1 hash
+ MessageDigest sha1;
+ try {
+ sha1 = MessageDigest.getInstance("SHA-1");
+ } catch (Exception e) {
+ throw new RuntimeException("SHA-1 not supported", e);
+ }
+
+ byte[] hash = sha1.digest(infoBytes);
+ return bytesToHex(hash);
+ }
+
+ public static void saveToFile(byte[] data, File outputFile) throws IOException {
+ try (FileOutputStream fos = new FileOutputStream(outputFile)) {
+ fos.write(data);
+ }
+ }
+
+ private static byte[] readBytes(File file) throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ return in.readAllBytes();
+ }
+ }
+
+ private static String bytesToHex(byte[] hash) {
+ StringBuilder hex = new StringBuilder();
+ for (byte b : hash) {
+ hex.append(String.format("%02x", b));
+ }
+ return hex.toString();
+ }
}