add torrent upload(finished) and tracker announce(unimplemnt)

Change-Id: I017c03df2bc1c40c4a080837821d56dfe58d6eb6
diff --git a/.gitignore b/.gitignore
index 549e00a..d9e9af4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,6 @@
 
 ### VS Code ###
 .vscode/
+
+### Upload ###
+./uploaded-torrents/
\ No newline at end of file
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();
+    }
 }
diff --git a/src/main/resources/mapper/TorrentMapper.xml b/src/main/resources/mapper/TorrentMapper.xml
new file mode 100644
index 0000000..9b53d29
--- /dev/null
+++ b/src/main/resources/mapper/TorrentMapper.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.example.g8backend.mapper.TorrentMapper">
+    <insert id="insertTorrent" >
+        INSERT INTO torrents (user_id, torrent_name, info_hash, file_size)
+        VALUES (#{userId}, #{torrentName}, UNHEX(#{infoHash}), #{fileSize})
+    </insert>
+
+    <select id="getTorrentByInfoHash" resultType="com.example.g8backend.entity.Torrent">
+        SELECT
+            torrent_id,
+            user_id,
+            torrent_name,
+            HEX(info_hash) AS infoHash,
+            file_size
+        FROM torrents
+        WHERE info_hash = UNHEX(#{infoHash})
+    </select>
+
+    <select id="getTorrentByTorrentId" resultType="com.example.g8backend.entity.Torrent">
+        SELECT
+            torrent_id,
+            user_id,
+            torrent_name,
+            HEX(info_hash) AS infoHash,
+            file_size
+        FROM torrents
+        WHERE torrent_id = #{torrentId}
+    </select>
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml
index 314c087..9627647 100644
--- a/src/main/resources/mapper/UserMapper.xml
+++ b/src/main/resources/mapper/UserMapper.xml
@@ -9,4 +9,7 @@
     <select id="getUserByEmail" resultType="com.example.g8backend.entity.User">
         SELECT * FROM users WHERE email = #{email}
     </select>
+    <select id="getUserByPasskey" resultType="com.example.g8backend.entity.User">
+        SELECT * FROM users WHERE passkey = #{passkey}
+    </select>
 </mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 2018117..365f9e7 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -2,7 +2,8 @@
   user_id INT AUTO_INCREMENT PRIMARY KEY,
   user_name VARCHAR(255) NOT NULL,
   password VARCHAR(255) NOT NULL,
-  email VARCHAR(255) NOT NULL UNIQUE
+  email VARCHAR(255) NOT NULL UNIQUE,
+  passkey VARCHAR(255) NOT NULL UNIQUE
 );
 
 CREATE TABLE IF NOT EXISTS `torrents` (
@@ -15,7 +16,7 @@
 );
 
 CREATE TABLE IF NOT EXISTS `peers` (
-  user_id INT NOT NULL,
+  passkey VARCHAR(255) NOT NULL,
   info_hash BINARY(20) NOT NULL,
   peer_id VARCHAR(20) NOT NULL,
   ip_address VARCHAR(128) NOT NULL,
@@ -23,6 +24,6 @@
   uploaded FLOAT NOT NULL,
   downloaded FLOAT NOT NULL,
   last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-  FOREIGN KEY (user_id) REFERENCES users(user_id),
-  PRIMARY KEY (user_id, info_hash, peer_id)
+  FOREIGN KEY (passkey) REFERENCES users(passkey),
+  PRIMARY KEY (passkey, info_hash, peer_id)
 );
diff --git a/uploaded-torrents/upload-1263176031835183570.torrent b/uploaded-torrents/upload-1263176031835183570.torrent
new file mode 100644
index 0000000..8634e79
--- /dev/null
+++ b/uploaded-torrents/upload-1263176031835183570.torrent
Binary files differ