blob: 11e02651d66df02ba681656e9f2ef65c7a65d930 [file] [log] [blame]
package tracker;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.persistence.*;
import com.dampcake.bencode.Bencode;
import com.dampcake.bencode.Type;
import com.querydsl.jpa.impl.JPAUpdateClause;
import entity.*;
import entity.config;
import java.util.Scanner;
import java.io.IOException;
public class Tracker implements TrackerInterface {
private final EntityManagerFactory emf;
// 默认构造:产线数据库
public Tracker() {
config cfg = new config();
Map<String,Object> props = new HashMap<>();
props.put("javax.persistence.jdbc.url",
"jdbc:mysql://" + cfg.SqlURL + "/" + cfg.Database);
props.put("javax.persistence.jdbc.user", cfg.SqlUsername);
props.put("javax.persistence.jdbc.password", cfg.SqlPassword);
this.emf = Persistence.createEntityManagerFactory("myPersistenceUnit", props);
}
// 测试传入:测试库
public Tracker(EntityManagerFactory emf) {
this.emf = emf;
}
@Override
public boolean AddUpLoad(String userid, int upload, String infoHash) {
long newTotal = upload; // convert to long
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// 1) find the seedId by infoHash
String seedId = em.createQuery(
"SELECT s.seedId FROM SeedHash s WHERE s.infoHash = :ih", String.class)
.setParameter("ih", infoHash)
.getSingleResult();
// 2) sum existing uploads for this user+seed
Long sumSoFar = em.createQuery(
"SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid AND t.seedid = :sid",
Long.class)
.setParameter("uid", userid)
.setParameter("sid", seedId)
.getSingleResult();
long delta = newTotal - sumSoFar;
if (delta < 0L) {
tx.rollback();
return false; // error: newTotal less than already recorded
}
if (delta == 0L) {
tx.rollback();
return false; // nothing to do
}
// 3) persist a new TransRecord with only the delta
TransRecord rd = new TransRecord();
rd.taskid = UUID.randomUUID().toString();
rd.uploaduserid = userid;
rd.seedid = seedId;
rd.upload = delta;
rd.maxupload = newTotal;
em.persist(rd);
em.flush();
// 4) 重新计算用户的总上传,确保与 TransRecord 完全一致
Long totalUpload = em.createQuery(
"SELECT COALESCE(SUM(t.upload),0) FROM TransRecord t WHERE t.uploaduserid = :uid",
Long.class
)
.setParameter("uid", userid)
.getSingleResult();
Long PTuploadbefor = em.createQuery(
"SELECT t.upload FROM UserPT t WHERE t.userid = :uid",
Long.class
).setParameter("uid", userid)
.getSingleResult();
UserPT user = em.find(UserPT.class, userid);
user.upload = totalUpload;
em.merge(user);
em.flush();
tx.commit();
Long PTuploadafter = em.createQuery(
"SELECT t.upload FROM UserPT t WHERE t.userid = :uid",
Long.class
).setParameter("uid", userid)
.getSingleResult();
System.out.println("按回车继续...");
System.out.printf("thisadd:%d userptsofar:%d userptafter:%d totaluploadnow:%d delta:%d%n",upload, PTuploadbefor,PTuploadafter, totalUpload,delta);
System.out.println("按回车继续...");
return false; // success
} catch (RuntimeException ex) {
if (tx.isActive()) tx.rollback();
throw ex;
} finally {
em.close();
}
}
@Override
public boolean ReduceUpLoad(String userid, int upload){
long uploadLong = upload; // convert to long
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
// 1) fetch user and ensure enough upload to reduce
UserPT user = em.find(UserPT.class, userid);
long before = user.upload;
if (uploadLong > before) {
tx.rollback();
return true; // error: cannot reduce more than current total
}
// 2) subtract
user.upload = before - uploadLong;
em.merge(user);
// (optional) record a negative TransRecord so sums stay in sync
TransRecord rd = new TransRecord();
rd.taskid = UUID.randomUUID().toString();
rd.uploaduserid = userid;
rd.seedid = null;
rd.upload = -uploadLong;
rd.maxupload = user.upload;
em.persist(rd);
tx.commit();
return false; // success
} catch (RuntimeException ex) {
if (tx.isActive()) tx.rollback();
throw ex;
} finally {
em.close();
}
}
@Override
public boolean AddDownload(String userid, int download, String infoHash) {
long newTotal = download; // convert to long
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
// 1. 查 SeedHash
TypedQuery<SeedHash> qsh = em.createQuery(
"SELECT s FROM SeedHash s WHERE s.infoHash = :h", SeedHash.class);
qsh.setParameter("h", infoHash);
List<SeedHash> shl = qsh.getResultList();
if (shl.isEmpty()) {
System.out.println("seed没有被记录");
return false;
}
String seedid = shl.get(0).seedId;
// 2. 统计该用户在该种子上的已有 download
TypedQuery<Long> qsum = em.createQuery(
"SELECT COALESCE(SUM(t.download),0) FROM TransRecord t " +
"WHERE t.seedid = :sid AND t.downloaduserid = :uid", Long.class);
qsum.setParameter("sid", seedid);
qsum.setParameter("uid", userid);
long oldSeedSum = qsum.getSingleResult();
long diff = newTotal - oldSeedSum;
if (diff <= 0) return false;
System.out.println("AddDownload: 该种子原有总量=" + oldSeedSum + ", 新总量=" + newTotal + ", 增量=" + diff);
try {
tx.begin();
// 1. persist 增量记录
TransRecord tr = new TransRecord();
tr.taskid = UUID.randomUUID().toString();
tr.downloaduserid = userid;
tr.seedid = seedid;
tr.download = diff;
tr.maxdownload = newTotal;
em.persist(tr);
// 2. 全表重新累计该用户所有种子的 download,并更新 UserPT.download
TypedQuery<Long> qTotal = em.createQuery(
"SELECT COALESCE(SUM(t.download),0) FROM TransRecord t WHERE t.downloaduserid = :uid",
Long.class
)
.setParameter("uid", userid);
long userTotalDownload = qTotal.getSingleResult();
QUserPT quser = QUserPT.userPT;
new JPAUpdateClause(em, quser)
.where(quser.userid.eq(userid))
.set(quser.download, userTotalDownload)
.execute();
tx.commit();
return false;
} catch (Exception e) {
if (tx.isActive()) tx.rollback();
return true;
} finally {
em.close();
}
} catch (Exception e) {
return true;
}
}
@Override
public boolean ReduceDownload(String userid, int download) {
long downloadLong = download; // convert to long
EntityManager em = emf.createEntityManager();
try {
// 1. 预检查当前值
TypedQuery<Long> qcurr = em.createQuery(
"SELECT u.download FROM UserPT u WHERE u.userid = :uid", Long.class);
qcurr.setParameter("uid", userid);
long current = qcurr.getSingleResult();
if (downloadLong > current) {
em.close();
return false;
}
// 2. 执行减法更新
EntityTransaction tx = em.getTransaction();
tx.begin();
QUserPT q = QUserPT.userPT;
new JPAUpdateClause(em, q)
.where(q.userid.eq(userid))
.set(q.download, q.download.subtract(downloadLong))
.execute();
tx.commit();
return false;
} catch(Exception e) {
return true;
} finally {
if (em.isOpen()) em.close();
}
}
@Override
public boolean AddMagic(String userid, int magic) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
QUserPT q = QUserPT.userPT;
long updated = new JPAUpdateClause(em, q)
.where(q.userid.eq(userid))
.set(q.magic, q.magic.add(magic))
.execute();
tx.commit();
return updated <= 0;
} catch(Exception e) {
if (tx.isActive()) tx.rollback();
return true;
} finally {
em.close();
}
}
@Override
public boolean ReduceMagic(String userid, int magic) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
QUserPT q = QUserPT.userPT;
long updated = new JPAUpdateClause(em, q)
.where(q.userid.eq(userid))
.set(q.magic, q.magic.subtract(magic))
.execute();
tx.commit();
return updated <= 0;
} catch(Exception e) {
if (tx.isActive()) tx.rollback();
return true;
} finally {
em.close();
}
}
@Override
public int SaveTorrent(String seedid, File TTorent){
try {
Path storageDir = Paths.get(config.TORRENT_STORAGE_DIR);
if (!Files.exists(storageDir)) {
Files.createDirectories(storageDir);
}
String filename = TTorent.getName();
Path target = storageDir.resolve(seedid + "_" + filename);
Files.copy(TTorent.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
// Calculate infoHash using ISO_8859_1 encoding method to match qBittorrent
String infoHash = null;
try {
infoHash = calculateInfoHashReencoding(target.toFile());
System.out.println("InfoHash (ISO_8859_1): " + infoHash);
} catch (Exception e) {
System.err.println("Warning: could not parse torrent infoHash: " + e.getMessage());
// Fallback to direct extraction method
try {
infoHash = calculateInfoHashDirect(target.toFile());
System.out.println("InfoHash (Direct): " + infoHash);
} catch (Exception e2) {
System.err.println("Warning: fallback infoHash calculation also failed: " + e2.getMessage());
}
}
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
Seed seed = em.find(Seed.class, seedid);
seed.url = target.toString();
em.merge(seed);
// upsert SeedHash only if we have a valid infoHash
if (infoHash != null) {
SeedHash sh = new SeedHash();
sh.seedId = seedid;
sh.infoHash = infoHash;
em.merge(sh);
}
tx.commit();
return 0;
} catch (Exception e) {
if (tx.isActive()) tx.rollback();
return 1;
} finally {
em.close();
}
} catch (Exception e) {
return 1;
}
}
@Override
public File GetTTorent(String seedid, String userid) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
File file = null;
try {
Seed seed = em.find(Seed.class, seedid);
if (seed == null || seed.url == null) {
return null;
}
file = new File(seed.url);
if (!file.exists()) {
return null;
}
tx.begin();
SeedDownload sd = new SeedDownload();
sd.seedId = seedid;
sd.userId = userid;
LocalDateTime now = LocalDateTime.now();
sd.downloadStart = now;
sd.downloadEnd = now;
em.persist(sd);
tx.commit();
} catch (Exception e) {
if (tx.isActive()) tx.rollback();
// ignore persistence errors and still return the file
} finally {
em.close();
}
return file;
}
@Override
public int AddRecord(TransRecord rd){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
em.persist(rd);
tx.commit();
// 返回1表示插入成功
return 1;
} catch (Exception e) {
if (tx.isActive()) tx.rollback();
return -1;
} finally {
em.close();
}
}
/**
* Calculate infoHash by extracting the original info dictionary bytes
* from the torrent file, rather than re-encoding the parsed data.
* This method preserves the original binary representation.
*/
private String calculateInfoHashDirect(File torrentFile) throws Exception {
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
// Find the info dictionary in the raw torrent data
int infoStart = findInfoDictionary(torrentData);
if (infoStart == -1) {
throw new Exception("Could not find info dictionary in torrent file");
}
// Extract the info dictionary bytes directly from the original torrent
byte[] infoBytes = extractInfoBytes(torrentData, infoStart);
// Debug: print first few bytes of info dict
System.out.print("Info dict starts with: ");
for (int i = 0; i < Math.min(20, infoBytes.length); i++) {
System.out.printf("%02x ", infoBytes[i] & 0xff);
}
System.out.println();
// Calculate SHA1 hash
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] digest = sha1.digest(infoBytes);
// Convert to hex string
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/**
* Correct method using ISO_8859_1 encoding for infohash calculation
* This matches qBittorrent's calculation method
*/
private String calculateInfoHashReencoding(File torrentFile) throws Exception {
byte[] torrentData = Files.readAllBytes(torrentFile.toPath());
// Use ISO_8859_1 charset for infohash calculation (as per BitTorrent specification)
Bencode bencodeInfoHash = new Bencode(java.nio.charset.StandardCharsets.ISO_8859_1);
@SuppressWarnings("unchecked")
Map<String,Object> meta = bencodeInfoHash.decode(torrentData, Type.DICTIONARY);
@SuppressWarnings("unchecked")
Map<String,Object> info = (Map<String,Object>) meta.get("info");
if (info == null) {
throw new Exception("No info dictionary found");
}
// Re-encode the info dictionary using ISO_8859_1
byte[] infoBytes = bencodeInfoHash.encode(info);
// Calculate SHA1 hash
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
byte[] digest = sha1.digest(infoBytes);
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/**
* Find the position of "4:info" in the torrent data
*/
private int findInfoDictionary(byte[] data) {
byte[] pattern = "4:info".getBytes();
for (int i = 0; i <= data.length - pattern.length; i++) {
boolean found = true;
for (int j = 0; j < pattern.length; j++) {
if (data[i + j] != pattern[j]) {
found = false;
break;
}
}
if (found) {
return i;
}
}
return -1;
}
/**
* Extract the info dictionary bytes from the original torrent data
*/
private byte[] extractInfoBytes(byte[] data, int infoStart) throws Exception {
// Skip "4:info" to get to the actual dictionary content
int dictStart = infoStart + 6; // "4:info".length()
if (dictStart >= data.length || data[dictStart] != 'd') {
throw new Exception("Invalid info dictionary format");
}
// Find the matching 'e' that closes the info dictionary
int dictEnd = findMatchingEnd(data, dictStart);
if (dictEnd == -1) {
throw new Exception("Could not find end of info dictionary");
}
// Extract the info dictionary bytes (including 'd' and 'e')
int length = dictEnd - dictStart + 1;
byte[] infoBytes = new byte[length];
System.arraycopy(data, dictStart, infoBytes, 0, length);
return infoBytes;
}
/**
* Find the matching 'e' for a dictionary that starts with 'd'
*/
private int findMatchingEnd(byte[] data, int start) {
if (start >= data.length || data[start] != 'd') {
return -1;
}
int depth = 0;
int i = start;
while (i < data.length) {
byte b = data[i];
if (b == 'd' || b == 'l') {
// Dictionary or list start
depth++;
i++;
} else if (b == 'e') {
// Dictionary or list end
depth--;
if (depth == 0) {
return i;
}
i++;
} else if (b == 'i') {
// Integer: i<number>e
i++; // skip 'i'
while (i < data.length && data[i] != 'e') {
i++;
}
if (i < data.length) i++; // skip 'e'
} else if (b >= '0' && b <= '9') {
// String: <length>:<string>
int lengthStart = i;
while (i < data.length && data[i] >= '0' && data[i] <= '9') {
i++;
}
if (i < data.length && data[i] == ':') {
// Parse length
String lengthStr = new String(data, lengthStart, i - lengthStart);
int length = Integer.parseInt(lengthStr);
i++; // skip ':'
i += length; // skip string content
} else {
// Invalid format
return -1;
}
} else {
// Unknown character
i++;
}
}
return -1;
}
}