22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 1 | package com.pt.controller; |
| 2 | |
| 3 | import com.pt.constant.Constants; |
| 4 | import com.pt.entity.Resource; |
| 5 | import com.pt.entity.User; |
| 6 | import com.pt.service.ResourceService; |
| 7 | import com.pt.service.UserService; |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 8 | import com.pt.utils.BencodeCodec; |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 9 | import com.pt.utils.JWTUtils; |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 10 | import com.pt.utils.TorrentPasskeyModifier; |
| 11 | import jakarta.servlet.http.HttpServletRequest; |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 12 | import org.springframework.beans.factory.annotation.Autowired; |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 13 | import org.springframework.context.annotation.Bean; |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 14 | import org.springframework.http.ResponseEntity; |
| 15 | import org.springframework.web.bind.annotation.*; |
Edwardsamaxl | f1bf7ad | 2025-06-03 23:52:16 +0800 | [diff] [blame] | 16 | import org.springframework.web.multipart.MultipartFile; |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 17 | import org.springframework.mock.web.MockMultipartFile; |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 18 | |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 19 | import java.io.ByteArrayInputStream; |
| 20 | import java.io.ByteArrayOutputStream; |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 21 | import java.io.IOException; |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 22 | import java.nio.charset.StandardCharsets; |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 23 | import java.util.HashMap; |
| 24 | import java.util.List; |
| 25 | import java.util.Map; |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 26 | import java.io.File; |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 27 | |
| 28 | @RestController |
| 29 | @RequestMapping("/api/resource") |
| 30 | @CrossOrigin(origins = "*") |
| 31 | public class ResourceController { |
| 32 | |
| 33 | @Autowired |
| 34 | private ResourceService resourceService; |
yyyang | f786cfa | 2025-06-08 15:13:34 +0800 | [diff] [blame] | 35 | |
| 36 | @Autowired |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 37 | private UserService userService; |
| 38 | |
| 39 | @GetMapping("/list/all") |
| 40 | public ResponseEntity<?> getAllResources(@RequestHeader("token") String token, |
| 41 | @RequestParam("username") String username) { |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 42 | |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 43 | Map<String, Object> ans = new HashMap<>(); |
| 44 | |
| 45 | if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)){ |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 46 | ans.put("message", "Invalid token"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 47 | return ResponseEntity.badRequest().body(ans); |
| 48 | } |
| 49 | |
| 50 | List<Resource> resources = resourceService.getAllResources(); |
| 51 | if (resources.isEmpty()) { |
| 52 | return ResponseEntity.noContent().build(); |
| 53 | } |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 54 | ans.put("message", "Resources found"); |
| 55 | ans.put("data", Map.of( |
| 56 | "resources", resources |
| 57 | )); |
| 58 | return ResponseEntity.ok().body(ans); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 59 | } |
| 60 | |
| 61 | @GetMapping("/list/user") |
| 62 | public ResponseEntity<?> getUserResources(@RequestHeader("token") String token, |
| 63 | @RequestParam("username") String username) { |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 64 | |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 65 | Map<String, Object> ans = new HashMap<>(); |
| 66 | |
| 67 | if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)){ |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 68 | ans.put("message", "Invalid token"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 69 | return ResponseEntity.badRequest().body(ans); |
| 70 | } |
| 71 | |
| 72 | List<Resource> resources = resourceService.getResourcesByAuthor(username); |
| 73 | if (resources.isEmpty()) { |
| 74 | return ResponseEntity.noContent().build(); |
| 75 | } |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 76 | ans.put("message", "User resources found"); |
| 77 | ans.put("data", Map.of( |
| 78 | "resources", resources |
| 79 | )); |
| 80 | return ResponseEntity.ok().body(ans); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 81 | } |
| 82 | |
| 83 | @PostMapping("/publish") |
Edwardsamaxl | f1bf7ad | 2025-06-03 23:52:16 +0800 | [diff] [blame] | 84 | public ResponseEntity<?> publishResource( |
| 85 | @RequestHeader("token") String token, |
| 86 | @RequestParam("username") String username, |
Edwardsamaxl | f1bf7ad | 2025-06-03 23:52:16 +0800 | [diff] [blame] | 87 | @RequestParam("description") String description, |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 88 | @RequestParam("torrent") MultipartFile torrentFile |
| 89 | ) |
| 90 | { |
Edwardsamaxl | f1bf7ad | 2025-06-03 23:52:16 +0800 | [diff] [blame] | 91 | |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 92 | Map<String, Object> ans = new HashMap<>(); |
| 93 | |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 94 | if (!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) { |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 95 | ans.put("message", "Invalid token"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 96 | return ResponseEntity.badRequest().body(ans); |
| 97 | } |
| 98 | |
| 99 | User user = userService.findByUsername(username); |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 100 | if (user == null || user.getLevel() < 2) { |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 101 | ans.put("message", "Insufficient permissions to publish resources"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 102 | return ResponseEntity.status(403).body(ans); |
| 103 | } |
| 104 | |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 105 | try { |
Edwardsamaxl | f1bf7ad | 2025-06-03 23:52:16 +0800 | [diff] [blame] | 106 | // 传入种子文件字节,同时传入资源其他信息 |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 107 | resourceService.publishResource(torrentFile.getOriginalFilename(), description, username, torrentFile.getBytes(), username); |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 108 | } catch (Exception e) { |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 109 | ans.put("message", "Failed to publish resource: " + e.getMessage()); |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 110 | return ResponseEntity.status(500).body(ans); |
| 111 | } |
22301102 | fe5f841 | 2025-06-01 17:25:51 +0800 | [diff] [blame] | 112 | |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 113 | ans.put("message", "Resource published successfully"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 114 | return ResponseEntity.ok(ans); |
| 115 | } |
| 116 | |
| 117 | @GetMapping("/get/{resourceId}") |
| 118 | public ResponseEntity<?> getResourceById(@PathVariable("resourceId") int resourceId, |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 119 | @RequestHeader("token") String token, |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 120 | @RequestParam("username") String username) { |
| 121 | |
| 122 | Map<String, Object> ans = new HashMap<>(); |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 123 | if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) { |
| 124 | ans.put("message", "Invalid token"); |
| 125 | return ResponseEntity.badRequest().body(ans); |
| 126 | } |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 127 | |
| 128 | Resource resource = resourceService.getResourceById(resourceId); |
| 129 | if (resource == null) { |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 130 | ans.put("message", "Resource not found"); |
| 131 | return ResponseEntity.badRequest().body(ans); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 132 | } |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 133 | |
| 134 | ans.put("message", "Resource found"); |
| 135 | ans.put("data", Map.of( |
| 136 | "resourceId", resource.getResourceId(), |
| 137 | "name", resource.getName(), |
| 138 | "description", resource.getDescription(), |
| 139 | "author", resource.getAuthor(), |
| 140 | "publishTime", resource.getPublishTime() |
| 141 | )); |
| 142 | |
| 143 | return ResponseEntity.ok().body(ans); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 144 | } |
| 145 | |
| 146 | @GetMapping("/download/{resourceId}") |
| 147 | public ResponseEntity<?> downloadResource(@PathVariable("resourceId") int resourceId, |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 148 | @RequestHeader("token") String token, |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 149 | @RequestParam("username") String username) throws IOException { |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 150 | |
| 151 | Map<String, Object> ans = new HashMap<>(); |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 152 | if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) { |
| 153 | ans.put("message", "Invalid token"); |
| 154 | return ResponseEntity.badRequest().body(ans); |
| 155 | } |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 156 | |
yyyang | f786cfa | 2025-06-08 15:13:34 +0800 | [diff] [blame] | 157 | User user = userService.findByUsername(username); |
| 158 | if (user == null) { |
| 159 | ans.put("message", "User not found"); |
| 160 | return ResponseEntity.status(404).body(ans); |
| 161 | } |
| 162 | |
yyyang | f786cfa | 2025-06-08 15:13:34 +0800 | [diff] [blame] | 163 | long uploaded = user.getUploaded(); |
| 164 | long downloaded = user.getDownloaded(); |
| 165 | |
| 166 | // 如果用户的下载量超过豁免值,则检查其共享率 |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 167 | if (downloaded > Constants.DOWNLOAD_EXEMPTION_BYTES) { |
yyyang | f786cfa | 2025-06-08 15:13:34 +0800 | [diff] [blame] | 168 | // 防止除以零 |
| 169 | double shareRatio = (downloaded == 0) ? Double.MAX_VALUE : (double) uploaded / downloaded; |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 170 | if (shareRatio < Constants.MIN_SHARE_RATIO_THRESHOLD) { |
yyyang | f786cfa | 2025-06-08 15:13:34 +0800 | [diff] [blame] | 171 | ans.put("message", "Share ratio is too low. Please seed more to improve your ratio before downloading new resources."); |
| 172 | ans.put("current_share_ratio", String.format("%.2f", shareRatio)); |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 173 | ans.put("required_share_ratio", Constants.MIN_SHARE_RATIO_THRESHOLD); |
yyyang | f786cfa | 2025-06-08 15:13:34 +0800 | [diff] [blame] | 174 | return ResponseEntity.status(403).body(ans); // 403 Forbidden |
| 175 | } |
| 176 | } |
| 177 | |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 178 | Resource resource = resourceService.getResourceById(resourceId); |
| 179 | byte[] file = resourceService.getTorrentFileByResource(resource, username); |
| 180 | if (file == null) { |
| 181 | return ResponseEntity.notFound().build(); |
| 182 | } |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 183 | |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 184 | TorrentPasskeyModifier modifier = new TorrentPasskeyModifier(); |
| 185 | |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 186 | return ResponseEntity.ok() |
| 187 | .header("Content-Type", "application/x-bittorrent") |
| 188 | .header("Content-Disposition", "attachment; filename=\"" + resource.getName() + ".torrent\"") |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 189 | .body(modifier.analyzeTorrentFile(file, username)); // 返回修改后的 torrent 文件 |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 190 | } |
| 191 | |
| 192 | @GetMapping("/search") |
| 193 | public ResponseEntity<?> searchResources(@RequestHeader("token") String token, |
| 194 | @RequestParam("username") String username, |
| 195 | @RequestParam("query") String query) { |
| 196 | Map<String, Object> ans = new HashMap<>(); |
| 197 | |
| 198 | if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)){ |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 199 | ans.put("message", "Invalid token"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 200 | return ResponseEntity.badRequest().body(ans); |
| 201 | } |
| 202 | |
| 203 | List<Resource> resources = resourceService.searchByQuery(query); |
| 204 | if (resources.isEmpty()) { |
| 205 | return ResponseEntity.noContent().build(); |
| 206 | } |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 207 | |
| 208 | ans.put("message", "Search results found"); |
| 209 | ans.put("data", Map.of( |
| 210 | "resources", resources |
| 211 | )); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 212 | return ResponseEntity.ok(resources); |
| 213 | } |
| 214 | |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 215 | @DeleteMapping("/delete") |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 216 | public ResponseEntity<?> deleteResource(@RequestHeader("token") String token, |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 217 | @RequestBody Map<String, Object> requestBody) { |
| 218 | String username = (String) requestBody.get("username"); |
| 219 | Integer resourceId = (Integer) requestBody.get("resourceId"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 220 | Map<String, Object> ans = new HashMap<>(); |
| 221 | Resource resource = resourceService.getResourceById(resourceId); |
| 222 | |
Edwardsamaxl | cba512d | 2025-06-09 21:17:29 +0800 | [diff] [blame^] | 223 | if(!JWTUtils.checkToken(token, username, Constants.UserRole.ADMIN) || resource == null) { |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 224 | ans.put("message", "Invalid token or insufficient permissions"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 225 | return ResponseEntity.badRequest().body(ans); |
| 226 | } |
| 227 | |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 228 | try { |
| 229 | resourceService.deleteResource(resourceId); |
| 230 | } catch (Exception e) { |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 231 | ans.put("message", "Failed to delete resource: " + e.getMessage()); |
22301102 | bc6da0a | 2025-06-02 17:47:29 +0800 | [diff] [blame] | 232 | return ResponseEntity.status(500).body(ans); |
| 233 | } |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 234 | |
22301102 | f69709e | 2025-06-08 14:10:02 +0800 | [diff] [blame] | 235 | ans.put("message", "Resource deleted successfully"); |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 236 | return ResponseEntity.ok(ans); |
| 237 | } |
22301102 | b108437 | 2025-06-01 16:44:23 +0800 | [diff] [blame] | 238 | } |