blob: 11e02651d66df02ba681656e9f2ef65c7a65d930 [file] [log] [blame]
rootcd436562025-05-08 14:09:19 +00001package tracker;
root0d8b11f2025-05-15 14:10:43 +00002import java.io.File;
rootd4959a82025-05-27 07:07:37 +00003import java.nio.file.Files;
4import java.nio.file.Path;
5import java.nio.file.Paths;
6import java.nio.file.StandardCopyOption;
TRM-codingd5de51e2025-06-08 03:27:01 +08007import java.security.MessageDigest;
rootd4959a82025-05-27 07:07:37 +00008import java.time.LocalDateTime;
TRM-codingd5de51e2025-06-08 03:27:01 +08009import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12import java.util.UUID;
13import javax.persistence.*;
14import com.dampcake.bencode.Bencode;
15import com.dampcake.bencode.Type;
16import com.querydsl.jpa.impl.JPAUpdateClause;
17import entity.*;
18import entity.config;
TRM-coding508b31f2025-06-09 02:07:14 +080019import java.util.Scanner;
20import java.io.IOException;
root0d8b11f2025-05-15 14:10:43 +000021public class Tracker implements TrackerInterface {
rootff0769a2025-05-18 17:24:41 +000022 private final EntityManagerFactory emf;
rootff0769a2025-05-18 17:24:41 +000023 // 默认构造:产线数据库
24 public Tracker() {
25 config cfg = new config();
26 Map<String,Object> props = new HashMap<>();
27 props.put("javax.persistence.jdbc.url",
28 "jdbc:mysql://" + cfg.SqlURL + "/" + cfg.Database);
29 props.put("javax.persistence.jdbc.user", cfg.SqlUsername);
30 props.put("javax.persistence.jdbc.password", cfg.SqlPassword);
31 this.emf = Persistence.createEntityManagerFactory("myPersistenceUnit", props);
32 }
rootff0769a2025-05-18 17:24:41 +000033 // 测试传入:测试库
34 public Tracker(EntityManagerFactory emf) {
35 this.emf = emf;
36 }
root0d8b11f2025-05-15 14:10:43 +000037 @Override
TRM-codingd5de51e2025-06-08 03:27:01 +080038 public boolean AddUpLoad(String userid, int upload, String infoHash) {
39 long newTotal = upload; // convert to long
root0d8b11f2025-05-15 14:10:43 +000040 EntityManager em = emf.createEntityManager();
41 EntityTransaction tx = em.getTransaction();
TRM-codingd5de51e2025-06-08 03:27:01 +080042 tx.begin();
root0d8b11f2025-05-15 14:10:43 +000043 try {
TRM-codingd5de51e2025-06-08 03:27:01 +080044 // 1) find the seedId by infoHash
45 String seedId = em.createQuery(
46 "SELECT s.seedId FROM SeedHash s WHERE s.infoHash = :ih", String.class)
47 .setParameter("ih", infoHash)
48 .getSingleResult();
49 // 2) sum existing uploads for this user+seed
50 Long sumSoFar = em.createQuery(
51 "SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid AND t.seedid = :sid",
52 Long.class)
53 .setParameter("uid", userid)
54 .setParameter("sid", seedId)
55 .getSingleResult();
56 long delta = newTotal - sumSoFar;
57 if (delta < 0L) {
58 tx.rollback();
TRM-coding508b31f2025-06-09 02:07:14 +080059 return false; // error: newTotal less than already recorded
TRM-codingd5de51e2025-06-08 03:27:01 +080060 }
61 if (delta == 0L) {
62 tx.rollback();
63 return false; // nothing to do
64 }
65 // 3) persist a new TransRecord with only the delta
66 TransRecord rd = new TransRecord();
67 rd.taskid = UUID.randomUUID().toString();
68 rd.uploaduserid = userid;
69 rd.seedid = seedId;
70 rd.upload = delta;
71 rd.maxupload = newTotal;
72 em.persist(rd);
TRM-coding508b31f2025-06-09 02:07:14 +080073 em.flush();
TRM-codingd5de51e2025-06-08 03:27:01 +080074 // 4) 重新计算用户的总上传,确保与 TransRecord 完全一致
75 Long totalUpload = em.createQuery(
76 "SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid",
77 Long.class
78 )
79 .setParameter("uid", userid)
80 .getSingleResult();
TRM-coding508b31f2025-06-09 02:07:14 +080081
82 Long PTuploadbefor = em.createQuery(
83 "SELECT t.upload FROM UserPT t WHERE t.userid = :uid",
84 Long.class
85 ).setParameter("uid", userid)
86 .getSingleResult();
87
88
TRM-codingd5de51e2025-06-08 03:27:01 +080089 UserPT user = em.find(UserPT.class, userid);
90 user.upload = totalUpload;
91 em.merge(user);
TRM-coding508b31f2025-06-09 02:07:14 +080092 em.flush();
root0d8b11f2025-05-15 14:10:43 +000093 tx.commit();
TRM-coding508b31f2025-06-09 02:07:14 +080094
95 Long PTuploadafter = em.createQuery(
96 "SELECT t.upload FROM UserPT t WHERE t.userid = :uid",
97 Long.class
98 ).setParameter("uid", userid)
99 .getSingleResult();
100
101 System.out.println("按回车继续...");
102 System.out.printf("thisadd:%d userptsofar:%d userptafter:%d totaluploadnow:%d delta:%d%n",upload, PTuploadbefor,PTuploadafter, totalUpload,delta);
103 System.out.println("按回车继续...");
TRM-codingd5de51e2025-06-08 03:27:01 +0800104 return false; // success
105 } catch (RuntimeException ex) {
root0d8b11f2025-05-15 14:10:43 +0000106 if (tx.isActive()) tx.rollback();
TRM-codingd5de51e2025-06-08 03:27:01 +0800107 throw ex;
root0d8b11f2025-05-15 14:10:43 +0000108 } finally {
109 em.close();
110 }
111 }
TRM-codingd5de51e2025-06-08 03:27:01 +0800112
root0d8b11f2025-05-15 14:10:43 +0000113 @Override
rootff0769a2025-05-18 17:24:41 +0000114 public boolean ReduceUpLoad(String userid, int upload){
TRM-codingd5de51e2025-06-08 03:27:01 +0800115 long uploadLong = upload; // convert to long
rootff0769a2025-05-18 17:24:41 +0000116 EntityManager em = emf.createEntityManager();
117 EntityTransaction tx = em.getTransaction();
TRM-codingd5de51e2025-06-08 03:27:01 +0800118 tx.begin();
rootff0769a2025-05-18 17:24:41 +0000119 try {
TRM-codingd5de51e2025-06-08 03:27:01 +0800120 // 1) fetch user and ensure enough upload to reduce
121 UserPT user = em.find(UserPT.class, userid);
122 long before = user.upload;
123 if (uploadLong > before) {
124 tx.rollback();
125 return true; // error: cannot reduce more than current total
126 }
127 // 2) subtract
128 user.upload = before - uploadLong;
129 em.merge(user);
130 // (optional) record a negative TransRecord so sums stay in sync
131 TransRecord rd = new TransRecord();
132 rd.taskid = UUID.randomUUID().toString();
133 rd.uploaduserid = userid;
134 rd.seedid = null;
135 rd.upload = -uploadLong;
136 rd.maxupload = user.upload;
137 em.persist(rd);
rootff0769a2025-05-18 17:24:41 +0000138 tx.commit();
TRM-codingd5de51e2025-06-08 03:27:01 +0800139 return false; // success
140 } catch (RuntimeException ex) {
rootff0769a2025-05-18 17:24:41 +0000141 if (tx.isActive()) tx.rollback();
TRM-codingd5de51e2025-06-08 03:27:01 +0800142 throw ex;
rootff0769a2025-05-18 17:24:41 +0000143 } finally {
144 em.close();
145 }
146 }
root0d8b11f2025-05-15 14:10:43 +0000147 @Override
TRM-codingd5de51e2025-06-08 03:27:01 +0800148 public boolean AddDownload(String userid, int download, String infoHash) {
149 long newTotal = download; // convert to long
rootf35409f2025-05-19 04:41:57 +0000150 EntityManager em = emf.createEntityManager();
151 EntityTransaction tx = em.getTransaction();
152 try {
TRM-codingd5de51e2025-06-08 03:27:01 +0800153 // 1. 查 SeedHash
154 TypedQuery<SeedHash> qsh = em.createQuery(
155 "SELECT s FROM SeedHash s WHERE s.infoHash = :h", SeedHash.class);
156 qsh.setParameter("h", infoHash);
157 List<SeedHash> shl = qsh.getResultList();
158 if (shl.isEmpty()) {
159 System.out.println("seed没有被记录");
TRM-coding508b31f2025-06-09 02:07:14 +0800160 return false;
TRM-codingd5de51e2025-06-08 03:27:01 +0800161 }
162 String seedid = shl.get(0).seedId;
163
164 // 2. 统计该用户在该种子上的已有 download
165 TypedQuery<Long> qsum = em.createQuery(
166 "SELECT COALESCE(SUM(t.download),0) FROM TransRecord t " +
167 "WHERE t.seedid = :sid AND t.downloaduserid = :uid", Long.class);
168 qsum.setParameter("sid", seedid);
169 qsum.setParameter("uid", userid);
170 long oldSeedSum = qsum.getSingleResult();
171
172 long diff = newTotal - oldSeedSum;
173 if (diff <= 0) return false;
174
175 System.out.println("AddDownload: 该种子原有总量=" + oldSeedSum + ", 新总量=" + newTotal + ", 增量=" + diff);
176
177 try {
178 tx.begin();
179 // 1. persist 增量记录
180 TransRecord tr = new TransRecord();
181 tr.taskid = UUID.randomUUID().toString();
182 tr.downloaduserid = userid;
183 tr.seedid = seedid;
184 tr.download = diff;
185 tr.maxdownload = newTotal;
186 em.persist(tr);
187
188 // 2. 全表重新累计该用户所有种子的 download,并更新 UserPT.download
189 TypedQuery<Long> qTotal = em.createQuery(
190 "SELECT COALESCE(SUM(t.download),0) FROM TransRecord t WHERE t.downloaduserid = :uid",
191 Long.class
192 )
193 .setParameter("uid", userid);
194 long userTotalDownload = qTotal.getSingleResult();
195 QUserPT quser = QUserPT.userPT;
196 new JPAUpdateClause(em, quser)
197 .where(quser.userid.eq(userid))
198 .set(quser.download, userTotalDownload)
199 .execute();
200
201 tx.commit();
202 return false;
203 } catch (Exception e) {
204 if (tx.isActive()) tx.rollback();
205 return true;
206 } finally {
207 em.close();
208 }
209 } catch (Exception e) {
rootf35409f2025-05-19 04:41:57 +0000210 return true;
rootf35409f2025-05-19 04:41:57 +0000211 }
212 }
root0d8b11f2025-05-15 14:10:43 +0000213 @Override
rootf35409f2025-05-19 04:41:57 +0000214 public boolean ReduceDownload(String userid, int download) {
TRM-codingd5de51e2025-06-08 03:27:01 +0800215 long downloadLong = download; // convert to long
rootf35409f2025-05-19 04:41:57 +0000216 EntityManager em = emf.createEntityManager();
rootf35409f2025-05-19 04:41:57 +0000217 try {
TRM-codingd5de51e2025-06-08 03:27:01 +0800218 // 1. 预检查当前值
219 TypedQuery<Long> qcurr = em.createQuery(
220 "SELECT u.download FROM UserPT u WHERE u.userid = :uid", Long.class);
221 qcurr.setParameter("uid", userid);
222 long current = qcurr.getSingleResult();
223 if (downloadLong > current) {
224 em.close();
TRM-coding508b31f2025-06-09 02:07:14 +0800225 return false;
TRM-codingd5de51e2025-06-08 03:27:01 +0800226 }
227 // 2. 执行减法更新
228 EntityTransaction tx = em.getTransaction();
rootf35409f2025-05-19 04:41:57 +0000229 tx.begin();
230 QUserPT q = QUserPT.userPT;
TRM-codingd5de51e2025-06-08 03:27:01 +0800231 new JPAUpdateClause(em, q)
rootf35409f2025-05-19 04:41:57 +0000232 .where(q.userid.eq(userid))
TRM-codingd5de51e2025-06-08 03:27:01 +0800233 .set(q.download, q.download.subtract(downloadLong))
rootf35409f2025-05-19 04:41:57 +0000234 .execute();
235 tx.commit();
TRM-codingd5de51e2025-06-08 03:27:01 +0800236 return false;
rootf35409f2025-05-19 04:41:57 +0000237 } catch(Exception e) {
rootf35409f2025-05-19 04:41:57 +0000238 return true;
239 } finally {
TRM-codingd5de51e2025-06-08 03:27:01 +0800240 if (em.isOpen()) em.close();
rootf35409f2025-05-19 04:41:57 +0000241 }
242 }
root0d8b11f2025-05-15 14:10:43 +0000243 @Override
rootf35409f2025-05-19 04:41:57 +0000244 public boolean AddMagic(String userid, int magic) {
245 EntityManager em = emf.createEntityManager();
246 EntityTransaction tx = em.getTransaction();
247 try {
248 tx.begin();
249 QUserPT q = QUserPT.userPT;
250 long updated = new JPAUpdateClause(em, q)
251 .where(q.userid.eq(userid))
252 .set(q.magic, q.magic.add(magic))
253 .execute();
254 tx.commit();
255 return updated <= 0;
256 } catch(Exception e) {
257 if (tx.isActive()) tx.rollback();
258 return true;
259 } finally {
260 em.close();
261 }
262 }
root0d8b11f2025-05-15 14:10:43 +0000263 @Override
rootf35409f2025-05-19 04:41:57 +0000264 public boolean ReduceMagic(String userid, int magic) {
265 EntityManager em = emf.createEntityManager();
266 EntityTransaction tx = em.getTransaction();
267 try {
268 tx.begin();
269 QUserPT q = QUserPT.userPT;
270 long updated = new JPAUpdateClause(em, q)
271 .where(q.userid.eq(userid))
272 .set(q.magic, q.magic.subtract(magic))
273 .execute();
274 tx.commit();
275 return updated <= 0;
276 } catch(Exception e) {
277 if (tx.isActive()) tx.rollback();
278 return true;
279 } finally {
280 em.close();
281 }
282 }
rootd4959a82025-05-27 07:07:37 +0000283 @Override
284 public int SaveTorrent(String seedid, File TTorent){
285 try {
286 Path storageDir = Paths.get(config.TORRENT_STORAGE_DIR);
287 if (!Files.exists(storageDir)) {
288 Files.createDirectories(storageDir);
289 }
290 String filename = TTorent.getName();
291 Path target = storageDir.resolve(seedid + "_" + filename);
292 Files.copy(TTorent.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
TRM-codingd5de51e2025-06-08 03:27:01 +0800293
tianruimingebb3dd02025-06-09 05:07:26 +0000294 // Calculate infoHash using ISO_8859_1 encoding method to match qBittorrent
TRM-codingd5de51e2025-06-08 03:27:01 +0800295 String infoHash = null;
296 try {
tianruimingebb3dd02025-06-09 05:07:26 +0000297 infoHash = calculateInfoHashReencoding(target.toFile());
298 System.out.println("InfoHash (ISO_8859_1): " + infoHash);
TRM-codingd5de51e2025-06-08 03:27:01 +0800299 } catch (Exception e) {
300 System.err.println("Warning: could not parse torrent infoHash: " + e.getMessage());
tianruimingebb3dd02025-06-09 05:07:26 +0000301 // Fallback to direct extraction method
302 try {
303 infoHash = calculateInfoHashDirect(target.toFile());
304 System.out.println("InfoHash (Direct): " + infoHash);
305 } catch (Exception e2) {
306 System.err.println("Warning: fallback infoHash calculation also failed: " + e2.getMessage());
307 }
TRM-codingd5de51e2025-06-08 03:27:01 +0800308 }
309
rootd4959a82025-05-27 07:07:37 +0000310 EntityManager em = emf.createEntityManager();
311 EntityTransaction tx = em.getTransaction();
312 try {
313 tx.begin();
314 Seed seed = em.find(Seed.class, seedid);
315 seed.url = target.toString();
316 em.merge(seed);
TRM-codingd5de51e2025-06-08 03:27:01 +0800317
318 // upsert SeedHash only if we have a valid infoHash
319 if (infoHash != null) {
320 SeedHash sh = new SeedHash();
321 sh.seedId = seedid;
322 sh.infoHash = infoHash;
323 em.merge(sh);
324 }
rootd4959a82025-05-27 07:07:37 +0000325 tx.commit();
326 return 0;
327 } catch (Exception e) {
328 if (tx.isActive()) tx.rollback();
329 return 1;
330 } finally {
331 em.close();
332 }
333 } catch (Exception e) {
334 return 1;
335 }
336 }
root0d8b11f2025-05-15 14:10:43 +0000337 @Override
Raveraae06122025-06-05 08:13:35 +0000338 public File GetTTorent(String seedid, String userid) {
rootd4959a82025-05-27 07:07:37 +0000339 EntityManager em = emf.createEntityManager();
340 EntityTransaction tx = em.getTransaction();
341 File file = null;
342 try {
343 Seed seed = em.find(Seed.class, seedid);
344 if (seed == null || seed.url == null) {
345 return null;
346 }
347 file = new File(seed.url);
348 if (!file.exists()) {
349 return null;
350 }
351 tx.begin();
352 SeedDownload sd = new SeedDownload();
353 sd.seedId = seedid;
354 sd.userId = userid;
rootd4959a82025-05-27 07:07:37 +0000355 LocalDateTime now = LocalDateTime.now();
356 sd.downloadStart = now;
357 sd.downloadEnd = now;
358 em.persist(sd);
359 tx.commit();
360 } catch (Exception e) {
361 if (tx.isActive()) tx.rollback();
362 // ignore persistence errors and still return the file
363 } finally {
364 em.close();
365 }
366 return file;
367 }
root0d8b11f2025-05-15 14:10:43 +0000368 @Override
rootf35409f2025-05-19 04:41:57 +0000369 public int AddRecord(TransRecord rd){
370 EntityManager em = emf.createEntityManager();
371 EntityTransaction tx = em.getTransaction();
372 try {
373 tx.begin();
374 em.persist(rd);
375 tx.commit();
TRM-codingd5de51e2025-06-08 03:27:01 +0800376 // 返回1表示插入成功
rootf35409f2025-05-19 04:41:57 +0000377 return 1;
378 } catch (Exception e) {
379 if (tx.isActive()) tx.rollback();
380 return -1;
381 } finally {
382 em.close();
383 }
384 }
tianruimingebb3dd02025-06-09 05:07:26 +0000385
386 /**
387 * Calculate infoHash by extracting the original info dictionary bytes
388 * from the torrent file, rather than re-encoding the parsed data.
389 * This method preserves the original binary representation.
390 */
391 private String calculateInfoHashDirect(File torrentFile) throws Exception {
392 byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
393
394 // Find the info dictionary in the raw torrent data
395 int infoStart = findInfoDictionary(torrentData);
396 if (infoStart == -1) {
397 throw new Exception("Could not find info dictionary in torrent file");
398 }
399
400 // Extract the info dictionary bytes directly from the original torrent
401 byte[] infoBytes = extractInfoBytes(torrentData, infoStart);
402
403 // Debug: print first few bytes of info dict
404 System.out.print("Info dict starts with: ");
405 for (int i = 0; i < Math.min(20, infoBytes.length); i++) {
406 System.out.printf("%02x ", infoBytes[i] & 0xff);
407 }
408 System.out.println();
409
410 // Calculate SHA1 hash
411 MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
412 byte[] digest = sha1.digest(infoBytes);
413
414 // Convert to hex string
415 StringBuilder sb = new StringBuilder();
416 for (byte b : digest) {
417 sb.append(String.format("%02x", b & 0xff));
418 }
419
420 return sb.toString();
421 }
422
423 /**
424 * Correct method using ISO_8859_1 encoding for infohash calculation
425 * This matches qBittorrent's calculation method
426 */
427 private String calculateInfoHashReencoding(File torrentFile) throws Exception {
428 byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
429
430 // Use ISO_8859_1 charset for infohash calculation (as per BitTorrent specification)
431 Bencode bencodeInfoHash = new Bencode(java.nio.charset.StandardCharsets.ISO_8859_1);
432
433 @SuppressWarnings("unchecked")
434 Map<String,Object> meta = bencodeInfoHash.decode(torrentData, Type.DICTIONARY);
435 @SuppressWarnings("unchecked")
436 Map<String,Object> info = (Map<String,Object>) meta.get("info");
437
438 if (info == null) {
439 throw new Exception("No info dictionary found");
440 }
441
442 // Re-encode the info dictionary using ISO_8859_1
443 byte[] infoBytes = bencodeInfoHash.encode(info);
444
445 // Calculate SHA1 hash
446 MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
447 byte[] digest = sha1.digest(infoBytes);
448
449 StringBuilder sb = new StringBuilder();
450 for (byte b : digest) {
451 sb.append(String.format("%02x", b & 0xff));
452 }
453
454 return sb.toString();
455 }
456
457 /**
458 * Find the position of "4:info" in the torrent data
459 */
460 private int findInfoDictionary(byte[] data) {
461 byte[] pattern = "4:info".getBytes();
462
463 for (int i = 0; i <= data.length - pattern.length; i++) {
464 boolean found = true;
465 for (int j = 0; j < pattern.length; j++) {
466 if (data[i + j] != pattern[j]) {
467 found = false;
468 break;
469 }
470 }
471 if (found) {
472 return i;
473 }
474 }
475 return -1;
476 }
477
478 /**
479 * Extract the info dictionary bytes from the original torrent data
480 */
481 private byte[] extractInfoBytes(byte[] data, int infoStart) throws Exception {
482 // Skip "4:info" to get to the actual dictionary content
483 int dictStart = infoStart + 6; // "4:info".length()
484
485 if (dictStart >= data.length || data[dictStart] != 'd') {
486 throw new Exception("Invalid info dictionary format");
487 }
488
489 // Find the matching 'e' that closes the info dictionary
490 int dictEnd = findMatchingEnd(data, dictStart);
491 if (dictEnd == -1) {
492 throw new Exception("Could not find end of info dictionary");
493 }
494
495 // Extract the info dictionary bytes (including 'd' and 'e')
496 int length = dictEnd - dictStart + 1;
497 byte[] infoBytes = new byte[length];
498 System.arraycopy(data, dictStart, infoBytes, 0, length);
499
500 return infoBytes;
501 }
502
503 /**
504 * Find the matching 'e' for a dictionary that starts with 'd'
505 */
506 private int findMatchingEnd(byte[] data, int start) {
507 if (start >= data.length || data[start] != 'd') {
508 return -1;
509 }
510
511 int depth = 0;
512 int i = start;
513
514 while (i < data.length) {
515 byte b = data[i];
516
517 if (b == 'd' || b == 'l') {
518 // Dictionary or list start
519 depth++;
520 i++;
521 } else if (b == 'e') {
522 // Dictionary or list end
523 depth--;
524 if (depth == 0) {
525 return i;
526 }
527 i++;
528 } else if (b == 'i') {
529 // Integer: i<number>e
530 i++; // skip 'i'
531 while (i < data.length && data[i] != 'e') {
532 i++;
533 }
534 if (i < data.length) i++; // skip 'e'
535 } else if (b >= '0' && b <= '9') {
536 // String: <length>:<string>
537 int lengthStart = i;
538 while (i < data.length && data[i] >= '0' && data[i] <= '9') {
539 i++;
540 }
541 if (i < data.length && data[i] == ':') {
542 // Parse length
543 String lengthStr = new String(data, lengthStart, i - lengthStart);
544 int length = Integer.parseInt(lengthStr);
545 i++; // skip ':'
546 i += length; // skip string content
547 } else {
548 // Invalid format
549 return -1;
550 }
551 } else {
552 // Unknown character
553 i++;
554 }
555 }
556
557 return -1;
558 }
root33a7d952025-05-18 17:24:41 +0000559}