diff --git a/build.gradle b/build.gradle index cac24f0a..551939ea 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'application' id 'checkstyle' id 'org.openjfx.javafxplugin' version '0.0.14' - id 'org.gradlex.extra-java-module-info' version '1.4.1' + id "org.hibernate.orm" version "6.5.0.Final" } group 'jtorrent' @@ -13,13 +13,7 @@ sourceCompatibility = '17' targetCompatibility = '17' application { - mainModule = 'jtorrent' mainClass = 'jtorrent.Main' - // Workaround for the following error: - // java.lang.IllegalAccessError: class ch.qos.logback.core.boolex.JaninoEventEvaluatorBase - // (in module ch.qos.logback.core) cannot access class org.codehaus.janino.ScriptEvaluator - // (in module org.codehaus.janino) because module ch.qos.logback.core does not read module org.codehaus.janino - applicationDefaultJvmArgs = ['--add-reads', 'ch.qos.logback.core=org.codehaus.janino'] } repositories { @@ -43,18 +37,13 @@ dependencies { implementation 'org.kordamp.ikonli:ikonli-materialdesign2-pack:12.3.1' implementation 'ch.qos.logback:logback-classic:1.5.6' implementation 'org.codehaus.janino:janino:3.1.12' + implementation 'com.h2database:h2:2.2.224' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' -} - -extraJavaModuleInfo { - module('com.dampcake:bencode', 'com.dampcake.bencode', '1.4') { - exports('com.dampcake.bencode') - } + testImplementation 'org.instancio:instancio-junit:4.5.1' } test { useJUnitPlatform() - jvmArgs = ['--add-reads', 'ch.qos.logback.core=org.codehaus.janino'] } \ No newline at end of file diff --git a/src/main/java/jtorrent/data/torrent/repository/FilePieceRepository.java b/src/main/java/jtorrent/data/torrent/repository/AppPieceRepository.java similarity index 97% rename from src/main/java/jtorrent/data/torrent/repository/FilePieceRepository.java rename to src/main/java/jtorrent/data/torrent/repository/AppPieceRepository.java index 5a22ed6f..3ed8e497 100644 --- a/src/main/java/jtorrent/data/torrent/repository/FilePieceRepository.java +++ b/src/main/java/jtorrent/data/torrent/repository/AppPieceRepository.java @@ -15,9 +15,9 @@ import jtorrent.domain.torrent.model.Torrent; import jtorrent.domain.torrent.repository.PieceRepository; -public class FilePieceRepository implements PieceRepository { +public class AppPieceRepository implements PieceRepository { - private static final Logger LOGGER = LoggerFactory.getLogger(FilePieceRepository.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AppPieceRepository.class); private static final String READ_ONLY_MODE = "r"; private static final String READ_WRITE_MODE = "rw"; diff --git a/src/main/java/jtorrent/data/torrent/repository/AppTorrentMetadataRepository.java b/src/main/java/jtorrent/data/torrent/repository/AppTorrentMetadataRepository.java new file mode 100644 index 00000000..3b9bafa6 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/repository/AppTorrentMetadataRepository.java @@ -0,0 +1,49 @@ +package jtorrent.data.torrent.repository; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.List; + +import jtorrent.data.torrent.source.file.filemanager.BencodedTorrentFileManager; +import jtorrent.data.torrent.source.file.model.BencodedTorrent; +import jtorrent.data.torrent.source.file.model.BencodedTorrentFactory; +import jtorrent.domain.torrent.model.TorrentMetadata; +import jtorrent.domain.torrent.repository.TorrentMetadataRepository; + +public class AppTorrentMetadataRepository implements TorrentMetadataRepository { + + private final BencodedTorrentFileManager torrentFileManager = new BencodedTorrentFileManager(); + + @Override + public TorrentMetadata getTorrentMetadata(File file) throws IOException { + return torrentFileManager.read(file).toDomain(); + } + + @Override + public TorrentMetadata getTorrentMetadata(URL url) throws IOException { + return torrentFileManager.read(url).toDomain(); + } + + @Override + public void saveTorrentMetadata(TorrentMetadata torrentMetadata, Path savePath) throws IOException { + BencodedTorrent bencodedTorrent = BencodedTorrent.fromDomain(torrentMetadata); + torrentFileManager.write(savePath, bencodedTorrent); + } + + /** + * Creates a new {@link TorrentMetadata} instance with the current time as the creation date. + * + * @param trackerUrls list of tiers, each containing a list of tracker URLs + * @param comment comment about the torrent + * @param createdBy name and version of the program used to create the .torrent + * @param pieceSize size of each piece in bytes + * @return a new {@link TorrentMetadata} instance + */ + @Override + public TorrentMetadata createTOrrentMetadata(Path source, List> trackerUrls, String comment, + String createdBy, int pieceSize) throws IOException { + return BencodedTorrentFactory.create(source, trackerUrls, comment, createdBy, pieceSize).toDomain(); + } +} diff --git a/src/main/java/jtorrent/data/torrent/repository/AppTorrentRepository.java b/src/main/java/jtorrent/data/torrent/repository/AppTorrentRepository.java new file mode 100644 index 00000000..c4ac3bb6 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/repository/AppTorrentRepository.java @@ -0,0 +1,70 @@ +package jtorrent.data.torrent.repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jtorrent.data.torrent.source.db.dao.TorrentDao; +import jtorrent.data.torrent.source.db.model.TorrentEntity; +import jtorrent.domain.common.util.Sha1Hash; +import jtorrent.domain.common.util.rx.MutableRxObservableList; +import jtorrent.domain.common.util.rx.RxObservableList; +import jtorrent.domain.torrent.model.Torrent; +import jtorrent.domain.torrent.repository.TorrentRepository; + +public class AppTorrentRepository implements TorrentRepository { + + private final MutableRxObservableList torrentsObservable; + private final Map infoHashToTorrent; + private final TorrentDao torrentDao = new TorrentDao(); + + public AppTorrentRepository() { + List torrents = new ArrayList<>(); + torrentDao.readAll().stream() + .map(TorrentEntity::toDomain) + .forEach(torrents::add); + infoHashToTorrent = torrents.stream() + .collect(HashMap::new, (map, torrent) -> map.put(torrent.getInfoHash(), torrent), Map::putAll); + this.torrentsObservable = new MutableRxObservableList<>(torrents); + } + + @Override + public RxObservableList getTorrents() { + return torrentsObservable; + } + + @Override + public Torrent getTorrent(Sha1Hash infoHash) { + return infoHashToTorrent.get(infoHash); + } + + @Override + public void addTorrent(Torrent torrent) { + if (isExistingTorrent(torrent)) { + // TODO: maybe throw exception if torrent already exists? + return; + } + torrentDao.create(TorrentEntity.fromDomain(torrent)); + infoHashToTorrent.put(torrent.getInfoHash(), torrent); + torrentsObservable.add(torrent); + } + + @Override + public void persistTorrents() { + infoHashToTorrent.values().stream() + .map(TorrentEntity::fromDomain) + .forEach(torrentDao::update); + } + + @Override + public void removeTorrent(Torrent torrent) { + torrentDao.delete(torrent.getInfoHash().getBytes()); + infoHashToTorrent.remove(torrent.getInfoHash()); + torrentsObservable.remove(torrent); + } + + private boolean isExistingTorrent(Torrent torrent) { + return infoHashToTorrent.containsKey(torrent.getInfoHash()); + } +} diff --git a/src/main/java/jtorrent/data/torrent/repository/FileTorrentRepository.java b/src/main/java/jtorrent/data/torrent/repository/FileTorrentRepository.java deleted file mode 100644 index 9d89b9a7..00000000 --- a/src/main/java/jtorrent/data/torrent/repository/FileTorrentRepository.java +++ /dev/null @@ -1,180 +0,0 @@ -package jtorrent.data.torrent.repository; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import jtorrent.data.torrent.model.BencodedFile; -import jtorrent.data.torrent.model.BencodedInfo; -import jtorrent.data.torrent.model.BencodedMultiFileInfo; -import jtorrent.data.torrent.model.BencodedSingleFileInfo; -import jtorrent.data.torrent.model.BencodedTorrent; -import jtorrent.domain.common.util.ContinuousMergedInputStream; -import jtorrent.domain.common.util.Sha1Hash; -import jtorrent.domain.common.util.rx.MutableRxObservableList; -import jtorrent.domain.common.util.rx.RxObservableList; -import jtorrent.domain.torrent.model.Torrent; -import jtorrent.domain.torrent.model.TorrentMetadata; -import jtorrent.domain.torrent.repository.TorrentRepository; - -public class FileTorrentRepository implements TorrentRepository { - - private final MutableRxObservableList torrents = new MutableRxObservableList<>(new ArrayList<>()); - private final Map infoHashToTorrent = new HashMap<>(); - - @Override - public void addTorrent(Torrent torrent) { - if (isExistingTorrent(torrent)) { - // TODO: maybe throw exception if torrent already exists? - return; - } - infoHashToTorrent.put(torrent.getInfoHash(), torrent); - torrents.add(torrent); - } - - @Override - public TorrentMetadata loadTorrent(File file) throws IOException { - InputStream inputStream = new FileInputStream(file); - return loadTorrent(inputStream); - } - - @Override - public TorrentMetadata loadTorrent(URL url) throws IOException { - // For some reason decoding directly from the URL stream doesn't work, so we have to read it into a byte array - // first. - try (BufferedInputStream in = new BufferedInputStream(url.openStream()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ) { - byte[] dataBuffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { - out.write(dataBuffer, 0, bytesRead); - } - - try (InputStream inputStream = new ByteArrayInputStream(out.toByteArray())) { - return loadTorrent(inputStream); - } - } - } - - private TorrentMetadata loadTorrent(InputStream inputStream) throws IOException { - return BencodedTorrent.decode(inputStream).toDomain(); - } - - @Override - public void saveTorrent(TorrentMetadata torrentMetadata, Path savePath) throws IOException { - BencodedTorrent bencodedTorrent = BencodedTorrent.fromDomain(torrentMetadata); - try (OutputStream outputStream = Files.newOutputStream(savePath)) { - outputStream.write(bencodedTorrent.bencode()); - } - } - - @Override - public void removeTorrent(Torrent torrent) { - infoHashToTorrent.remove(torrent.getInfoHash()); - torrents.remove(torrent); - } - - @Override - public RxObservableList getTorrents() { - return torrents; - } - - @Override - public Torrent getTorrent(Sha1Hash infoHash) { - return infoHashToTorrent.get(infoHash); - } - - /** - * Create a new {@link TorrentMetadata} instance with the current time as the creation date. - * - * @param trackerUrls list of tiers, each containing a list of tracker URLs - * @param comment comment about the torrent - * @param createdBy name and version of the program used to create the .torrent - * @param pieceSize size of each piece in bytes - * @return a new {@link TorrentMetadata} instance - */ - @Override - public TorrentMetadata createNewTorrent(Path source, List> trackerUrls, String comment, - String createdBy, int pieceSize) throws IOException { - Long creationDate = LocalDateTime.now().toEpochSecond(OffsetDateTime.now().getOffset()); - BencodedInfo info = buildBencodedInfo(source, pieceSize); - return BencodedTorrent.withAnnounceList(creationDate, trackerUrls, comment, createdBy, info).toDomain(); - } - - private static BencodedInfo buildBencodedInfo(Path source, int pieceSize) throws IOException { - if (Files.isDirectory(source)) { - return buildBencodedMultiFileInfo(source, pieceSize); - } else { - return buildBencondedSingleFileInfo(source, pieceSize); - } - } - - private static BencodedSingleFileInfo buildBencondedSingleFileInfo(Path source, int pieceSize) throws IOException { - if (Files.isDirectory(source)) { - throw new IllegalArgumentException("Source must be a file"); - } - - byte[] hashes = computeHashes(Files.newInputStream(source), pieceSize); - String fileName = source.getFileName().toString(); - long length = Files.size(source); - return new BencodedSingleFileInfo(pieceSize, hashes, fileName, length); - } - - private static BencodedMultiFileInfo buildBencodedMultiFileInfo(Path source, int pieceSize) throws IOException { - if (!Files.isDirectory(source)) { - throw new IllegalArgumentException("Source must be a directory"); - } - - List filePaths = getFilesInDirectory(source); - - List inputStreams = new ArrayList<>(); - List files = new ArrayList<>(); - for (Path filePath : filePaths) { - long length = Files.size(filePath); - files.add(BencodedFile.fromPath(source.relativize(filePath), length)); - inputStreams.add(Files.newInputStream(filePath)); - } - - byte[] hashes = computeHashes(new ContinuousMergedInputStream(inputStreams), pieceSize); - String dirName = source.getFileName().toString(); - return new BencodedMultiFileInfo(pieceSize, hashes, dirName, files); - } - - private static List getFilesInDirectory(Path directory) throws IOException { - try (Stream stream = Files.walk(directory)) { - return stream.filter(Files::isRegularFile).toList(); - } - } - - private static byte[] computeHashes(InputStream inputStream, int pieceSize) throws IOException { - List hashes = new ArrayList<>(); - int bytesRead; - byte[] buffer = new byte[pieceSize]; - while ((bytesRead = inputStream.read(buffer)) != -1) { - byte[] piece = Arrays.copyOf(buffer, bytesRead); - hashes.add(Sha1Hash.of(piece)); - } - return Sha1Hash.concatHashes(hashes); - } - - private boolean isExistingTorrent(Torrent torrent) { - return infoHashToTorrent.containsKey(torrent.getInfoHash()); - } -} diff --git a/src/main/java/jtorrent/data/torrent/source/db/dao/TorrentDao.java b/src/main/java/jtorrent/data/torrent/source/db/dao/TorrentDao.java new file mode 100644 index 00000000..fa73843f --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/dao/TorrentDao.java @@ -0,0 +1,57 @@ +package jtorrent.data.torrent.source.db.dao; + +import java.util.Arrays; +import java.util.List; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import jtorrent.data.torrent.source.db.model.TorrentEntity; +import jtorrent.data.torrent.source.db.util.HibernateUtil; + +public class TorrentDao { + + private final SessionFactory sessionFactory = HibernateUtil.getSessionFactory(); + + public void create(TorrentEntity torrentEntity) { + try (Session session = sessionFactory.openSession()) { + session.beginTransaction(); + session.persist(torrentEntity); + session.getTransaction().commit(); + } + } + + public List readAll() { + try (Session session = sessionFactory.openSession()) { + return session.createQuery("from TorrentEntity", TorrentEntity.class).list(); + } + } + + public TorrentEntity read(byte[] infoHash) { + try (Session session = sessionFactory.openSession()) { + TorrentEntity torrentEntity = session.get(TorrentEntity.class, infoHash); + if (torrentEntity == null) { + throw new IllegalArgumentException( + "TorrentEntity with infoHash " + Arrays.toString(infoHash) + " does not exist"); + } + return torrentEntity; + } + } + + public void update(TorrentEntity torrentEntity) { + try (Session session = sessionFactory.openSession()) { + session.beginTransaction(); + session.merge(torrentEntity); + session.getTransaction().commit(); + } + } + + public void delete(byte[] infoHash) { + try (Session session = sessionFactory.openSession()) { + session.beginTransaction(); + var entity = session.get(TorrentEntity.class, infoHash); + session.remove(entity); + session.getTransaction().commit(); + } + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/model/FileInfoComponent.java b/src/main/java/jtorrent/data/torrent/source/db/model/FileInfoComponent.java new file mode 100644 index 00000000..da37dfdd --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/model/FileInfoComponent.java @@ -0,0 +1,164 @@ +package jtorrent.data.torrent.source.db.model; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.hibernate.annotations.Formula; + +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Lob; +import jakarta.persistence.OrderColumn; +import jtorrent.domain.common.util.Sha1Hash; +import jtorrent.domain.torrent.model.FileInfo; +import jtorrent.domain.torrent.model.FileMetadata; +import jtorrent.domain.torrent.model.MultiFileInfo; +import jtorrent.domain.torrent.model.SingleFileInfo; + +@Embeddable +public class FileInfoComponent { + + @Column + private final String directory; + + @OrderColumn + @ElementCollection + private final List fileMetadata; + + @Lob + private final byte[] pieceHashes; + + @Column(nullable = false) + private final int pieceSize; + + @Formula("infoHash") + private final byte[] infoHash; + + protected FileInfoComponent() { + this(null, Collections.emptyList(), new byte[0], 0, null); + } + + public FileInfoComponent(String directory, List fileMetadata, byte[] pieceHashes, + int pieceSize, byte[] infoHash) { + this.directory = directory; + this.fileMetadata = fileMetadata; + this.pieceHashes = pieceHashes; + this.pieceSize = pieceSize; + this.infoHash = infoHash; + } + + public static FileInfoComponent fromDomain(FileInfo fileInfo) { + if (fileInfo instanceof SingleFileInfo singleFileInfo) { + return fromDomain(singleFileInfo); + } else { + return fromDomain((MultiFileInfo) fileInfo); + } + } + + public static FileInfoComponent fromDomain(SingleFileInfo singleFileInfo) { + List fileMetadata = singleFileInfo.getFileMetaData().stream() + .map(FileMetadataComponent::fromDomain) + .toList(); + byte[] pieceHashes = Sha1Hash.concatHashes(singleFileInfo.getPieceHashes()); + Sha1Hash.concatHashes(singleFileInfo.getPieceHashes()); + int pieceSize = singleFileInfo.getPieceSize(); + byte[] infoHash = singleFileInfo.getInfoHash().getBytes(); + return new FileInfoComponent(null, fileMetadata, pieceHashes, pieceSize, infoHash); + } + + public static FileInfoComponent fromDomain(MultiFileInfo multiFileInfo) { + String directory = multiFileInfo.getDirectory(); + List fileMetadata = multiFileInfo.getFileMetaData().stream() + .map(FileMetadataComponent::fromDomain) + .toList(); + byte[] pieceHashes = Sha1Hash.concatHashes(multiFileInfo.getPieceHashes()); + int pieceSize = multiFileInfo.getPieceSize(); + byte[] infoHash = multiFileInfo.getInfoHash().getBytes(); + return new FileInfoComponent(directory, fileMetadata, pieceHashes, pieceSize, infoHash); + } + + public FileInfo toDomain() { + return isSingleFile() ? toSingleFileInfo() : toMultiFileInfo(); + } + + private boolean isSingleFile() { + return directory == null; + } + + private SingleFileInfo toSingleFileInfo() { + FileMetadata domainFileMetadata = fileMetadata.get(0).toDomain(); + List domainPieceHashes = Sha1Hash.splitHashes(pieceHashes); + Sha1Hash domainInfoHash = new Sha1Hash(infoHash); + return new SingleFileInfo(domainFileMetadata, pieceSize, domainPieceHashes, domainInfoHash); + } + + private MultiFileInfo toMultiFileInfo() { + List domainFileMetadata = fileMetadata.stream() + .map(FileMetadataComponent::toDomain) + .toList(); + List domainPieceHashes = Sha1Hash.splitHashes(pieceHashes); + Sha1Hash domainInfoHash = new Sha1Hash(infoHash); + return new MultiFileInfo(directory, domainFileMetadata, pieceSize, domainPieceHashes, domainInfoHash); + } + + public String getDirectory() { + return directory; + } + + public List getFileMetadata() { + return fileMetadata; + } + + public byte[] getPieceHashes() { + return pieceHashes; + } + + public int getPieceSize() { + return pieceSize; + } + + public byte[] getInfoHash() { + return infoHash; + } + + @Override + public int hashCode() { + int result = Objects.hashCode(directory); + result = 31 * result + fileMetadata.hashCode(); + result = 31 * result + Arrays.hashCode(pieceHashes); + result = 31 * result + pieceSize; + result = 31 * result + Arrays.hashCode(infoHash); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FileInfoComponent that = (FileInfoComponent) o; + return pieceSize == that.pieceSize + && Objects.equals(directory, that.directory) + && fileMetadata.equals(that.fileMetadata) + && Arrays.equals(pieceHashes, that.pieceHashes) + && Arrays.equals(infoHash, that.infoHash); + } + + @Override + public String toString() { + return "FileInfoComponent{" + + "directory='" + directory + '\'' + + ", fileMetadata=" + fileMetadata + + ", pieceHashes=" + Arrays.toString(pieceHashes) + + ", pieceSize=" + pieceSize + + ", infoHash=" + Arrays.toString(infoHash) + + '}'; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/model/FileMetadataComponent.java b/src/main/java/jtorrent/data/torrent/source/db/model/FileMetadataComponent.java new file mode 100644 index 00000000..446d384f --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/model/FileMetadataComponent.java @@ -0,0 +1,143 @@ +package jtorrent.data.torrent.source.db.model; + +import java.nio.file.Path; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jtorrent.domain.torrent.model.FileMetadata; + +@Embeddable +public class FileMetadataComponent { + + @Column(nullable = false) + private final long size; + + @Column(nullable = false) + private final String path; + + @Column(nullable = false) + private final int firstPiece; + + @Column(nullable = false) + private final int firstPieceStart; + + @Column(nullable = false) + private final int lastPiece; + + @Column(nullable = false) + private final int lastPieceEnd; + + @Column(nullable = false) + private final long start; + + protected FileMetadataComponent() { + this(0, "", 0, 0, 0, 0, 0); + } + + public FileMetadataComponent(long size, String path, int firstPiece, int firstPieceStart, int lastPiece, + int lastPieceEnd, long start) { + this.size = size; + this.path = path; + this.firstPiece = firstPiece; + this.firstPieceStart = firstPieceStart; + this.lastPiece = lastPiece; + this.lastPieceEnd = lastPieceEnd; + this.start = start; + } + + public static FileMetadataComponent fromDomain(FileMetadata fileMetadata) { + return new FileMetadataComponent( + fileMetadata.size(), + fileMetadata.path().toString(), + fileMetadata.firstPiece(), + fileMetadata.firstPieceStart(), + fileMetadata.lastPiece(), + fileMetadata.lastPieceEnd(), + fileMetadata.start() + ); + } + + public FileMetadata toDomain() { + return new FileMetadata( + Path.of(path), + start, + size, + firstPiece, + firstPieceStart, + lastPiece, + lastPieceEnd + ); + } + + public long getSize() { + return size; + } + + public String getPath() { + return path; + } + + public int getFirstPiece() { + return firstPiece; + } + + public int getFirstPieceStart() { + return firstPieceStart; + } + + public int getLastPiece() { + return lastPiece; + } + + public int getLastPieceEnd() { + return lastPieceEnd; + } + + public long getStart() { + return start; + } + + @Override + public int hashCode() { + int result = Long.hashCode(size); + result = 31 * result + path.hashCode(); + result = 31 * result + firstPiece; + result = 31 * result + firstPieceStart; + result = 31 * result + lastPiece; + result = 31 * result + lastPieceEnd; + result = 31 * result + Long.hashCode(start); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FileMetadataComponent that = (FileMetadataComponent) o; + return size == that.size + && firstPiece == that.firstPiece + && firstPieceStart == that.firstPieceStart + && lastPiece == that.lastPiece + && lastPieceEnd == that.lastPieceEnd + && start == that.start + && path.equals(that.path); + } + + @Override + public String toString() { + return "FileMetadataComponent{" + + "size=" + size + + ", path='" + path + '\'' + + ", firstPiece=" + firstPiece + + ", firstPieceStart=" + firstPieceStart + + ", lastPiece=" + lastPiece + + ", lastPieceEnd=" + lastPieceEnd + + ", start=" + start + + '}'; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/model/TorrentEntity.java b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentEntity.java new file mode 100644 index 00000000..c0189202 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentEntity.java @@ -0,0 +1,143 @@ +package jtorrent.data.torrent.source.db.model; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jtorrent.domain.torrent.model.Torrent; +import jtorrent.domain.torrent.model.TorrentMetadata; +import jtorrent.domain.torrent.model.TorrentProgress; +import jtorrent.domain.torrent.model.TorrentStatistics; + +@Entity +public class TorrentEntity { + + @Column(nullable = false) + private final String displayName; + @Column(nullable = false) + private final String saveDirectory; + @Embedded + private final TorrentMetadataComponent metadata; + @Embedded + private final TorrentStatisticsComponent statistics; + @Embedded + private final TorrentProgressComponent progress; + @Enumerated + @Column(nullable = false) + private final Torrent.State state; + @Id + @Column(length = 20) + private byte[] infoHash; + + protected TorrentEntity() { + this(new byte[0], "", "", new TorrentMetadataComponent(), new TorrentStatisticsComponent(), + new TorrentProgressComponent(), Torrent.State.STOPPED); + } + + public TorrentEntity(byte[] infoHash, String displayName, String saveDirectory, TorrentMetadataComponent metadata, + TorrentStatisticsComponent statistics, TorrentProgressComponent progress, Torrent.State state) { + this.infoHash = infoHash; + this.displayName = displayName; + this.saveDirectory = saveDirectory; + this.metadata = metadata; + this.statistics = statistics; + this.progress = progress; + this.state = state; + } + + public static TorrentEntity fromDomain(Torrent torrent) { + return new TorrentEntity( + torrent.getInfoHash().getBytes(), + torrent.getName(), + torrent.getSaveDirectory().toString(), + TorrentMetadataComponent.fromDomain(torrent.getMetadata()), + TorrentStatisticsComponent.fromDomain(torrent.getStatistics()), + TorrentProgressComponent.fromDomain(torrent.getProgress()), + torrent.getState() + ); + } + + public Torrent toDomain() { + TorrentMetadata domainMetadata = metadata.toDomain(); + TorrentStatistics domainStatistics = statistics.toDomain(); + TorrentProgress domainProgress = progress.toDomain(domainMetadata.fileInfo()); + Path domainSaveDirectory = Paths.get(saveDirectory); + return new Torrent(domainMetadata, domainStatistics, domainProgress, displayName, domainSaveDirectory, state); + } + + public byte[] getInfoHash() { + return infoHash; + } + + public String getDisplayName() { + return displayName; + } + + public String getSaveDirectory() { + return saveDirectory; + } + + public TorrentMetadataComponent getMetadata() { + return metadata; + } + + public TorrentStatisticsComponent getStatistics() { + return statistics; + } + + public TorrentProgressComponent getProgress() { + return progress; + } + + public Torrent.State getState() { + return state; + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(infoHash); + result = 31 * result + displayName.hashCode(); + result = 31 * result + saveDirectory.hashCode(); + result = 31 * result + metadata.hashCode(); + result = 31 * result + statistics.hashCode(); + result = 31 * result + progress.hashCode(); + result = 31 * result + state.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TorrentEntity that = (TorrentEntity) o; + return Arrays.equals(infoHash, that.infoHash) + && displayName.equals(that.displayName) + && saveDirectory.equals(that.saveDirectory) + && metadata.equals(that.metadata) + && statistics.equals(that.statistics) + && progress.equals(that.progress) + && state == that.state; + } + + @Override + public String toString() { + return "TorrentEntity{" + + "infoHash=" + Arrays.toString(infoHash) + + ", displayName='" + displayName + '\'' + + ", saveDirectory='" + saveDirectory + '\'' + + ", metadata=" + metadata + + ", statistics=" + statistics + + ", progress=" + progress + + '}'; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/model/TorrentMetadataComponent.java b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentMetadataComponent.java new file mode 100644 index 00000000..704b1901 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentMetadataComponent.java @@ -0,0 +1,138 @@ +package jtorrent.data.torrent.source.db.model; + +import java.net.URI; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.OrderColumn; +import jtorrent.domain.torrent.model.FileInfo; +import jtorrent.domain.torrent.model.TorrentMetadata; + +@Embeddable +public class TorrentMetadataComponent { + + private static final String TRACKER_SEPARATOR = " "; + + /** + * The list of tracker tiers. Each tier is a String made up of tracker URLs separated by a space. + * This is a workaround to the fact that JPA does not support nested collections. + */ + @OrderColumn + @ElementCollection + private final List trackers; + + @Column(nullable = false) + private final LocalDateTime creationDate; + + @Column(nullable = false) + private final String comment; + + @Column(nullable = false) + private final String createdBy; + + @Embedded + private final FileInfoComponent fileInfo; + + protected TorrentMetadataComponent() { + this(Collections.emptyList(), LocalDateTime.MIN, "", "", new FileInfoComponent()); + } + + public TorrentMetadataComponent(List trackers, LocalDateTime creationDate, String comment, String createdBy, + FileInfoComponent fileInfo) { + this.trackers = trackers; + this.creationDate = creationDate; + this.comment = comment; + this.createdBy = createdBy; + this.fileInfo = fileInfo; + } + + public static TorrentMetadataComponent fromDomain(TorrentMetadata torrentMetadata) { + List trackerTiers = torrentMetadata.trackerTiers().stream() + .map(tier -> tier.stream() + .map(URI::toString) + .collect(Collectors.joining(TRACKER_SEPARATOR)) + ) + .toList(); + LocalDateTime creationDate = torrentMetadata.creationDate(); + String comment = torrentMetadata.comment(); + String createdBy = torrentMetadata.createdBy(); + FileInfoComponent fileInfo = FileInfoComponent.fromDomain(torrentMetadata.fileInfo()); + return new TorrentMetadataComponent(trackerTiers, creationDate, comment, createdBy, fileInfo); + } + + public TorrentMetadata toDomain() { + List> domainTrackers = trackers.stream() + .map(tier -> Arrays.stream(tier.split(TRACKER_SEPARATOR)) + .map(URI::create) + .toList() + ) + .toList(); + FileInfo domainFileInfo = fileInfo.toDomain(); + return new TorrentMetadata(domainTrackers, creationDate, comment, createdBy, domainFileInfo); + } + + public List getTrackers() { + return trackers; + } + + public LocalDateTime getCreationDate() { + return creationDate; + } + + public String getComment() { + return comment; + } + + public String getCreatedBy() { + return createdBy; + } + + public FileInfoComponent getFileInfo() { + return fileInfo; + } + + @Override + public int hashCode() { + int result = trackers.hashCode(); + result = 31 * result + creationDate.hashCode(); + result = 31 * result + comment.hashCode(); + result = 31 * result + createdBy.hashCode(); + result = 31 * result + fileInfo.hashCode(); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TorrentMetadataComponent that = (TorrentMetadataComponent) o; + return trackers.equals(that.trackers) + && creationDate.equals(that.creationDate) + && comment.equals(that.comment) + && createdBy.equals(that.createdBy) + && fileInfo.equals(that.fileInfo); + } + + @Override + public String toString() { + return "TorrentMetadataComponent{" + + "trackers=" + trackers + + ", creationDate=" + creationDate + + ", comment='" + comment + '\'' + + ", createdBy='" + createdBy + '\'' + + ", fileInfo=" + fileInfo + + '}'; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/model/TorrentProgressComponent.java b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentProgressComponent.java new file mode 100644 index 00000000..c9d6c94d --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentProgressComponent.java @@ -0,0 +1,137 @@ +package jtorrent.data.torrent.source.db.model; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Lob; +import jtorrent.domain.torrent.model.FileInfo; +import jtorrent.domain.torrent.model.FileMetadata; +import jtorrent.domain.torrent.model.FileProgress; +import jtorrent.domain.torrent.model.TorrentProgress; + +@Embeddable +public class TorrentProgressComponent { + + @Lob + @Column(nullable = false) + private final byte[] verifiedPieces; + + @Lob + @Column(nullable = false) + private byte[] pieceToReceivedBlocks; + + protected TorrentProgressComponent() { + this(new byte[0], new byte[0]); + } + + public TorrentProgressComponent(byte[] pieceToReceivedBlocks, byte[] verifiedPieces) { + this.pieceToReceivedBlocks = pieceToReceivedBlocks; + this.verifiedPieces = verifiedPieces; + } + + public static TorrentProgressComponent fromDomain(TorrentProgress torrentProgress) { + byte[] pieceToReceivedBlocks = serializeMap(torrentProgress.getReceivedBlocks()); + byte[] verifiedPieces = torrentProgress.getVerifiedPieces().toByteArray(); + return new TorrentProgressComponent(pieceToReceivedBlocks, verifiedPieces); + } + + private static byte[] serializeMap(Map map) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos)) { + dos.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) { + dos.writeInt(entry.getKey()); + byte[] bitsetBytes = entry.getValue().toByteArray(); + dos.writeInt(bitsetBytes.length); + dos.write(bitsetBytes); + } + return baos.toByteArray(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + public TorrentProgress toDomain(FileInfo fileInfo) { + BitSet domainVerifiedPieces = BitSet.valueOf(verifiedPieces); + Map domainFileProgress = fileInfo.getFileMetaData().stream() + .map(FileMetadata::path) + .collect( + Collectors.toMap( + Function.identity(), + path -> FileProgress.createExisting(fileInfo, fileInfo.getFileMetaData(path), + domainVerifiedPieces) + ) + ); + Map domainPieceToReceivedBlocks = deserializeMap(pieceToReceivedBlocks); + return TorrentProgress.createExisting(fileInfo, domainFileProgress, domainVerifiedPieces, + domainPieceToReceivedBlocks); + } + + private static Map deserializeMap(byte[] bytes) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais)) { + int size = dis.readInt(); + Map map = new HashMap<>(size); + for (int i = 0; i < size; i++) { + int key = dis.readInt(); + int length = dis.readInt(); + byte[] bitsetBytes = new byte[length]; + dis.readFully(bitsetBytes); + BitSet bitSet = BitSet.valueOf(bitsetBytes); + map.put(key, bitSet); + } + return map; + } catch (IOException e) { + throw new AssertionError(e); + } + } + + public byte[] getVerifiedPieces() { + return verifiedPieces; + } + + public byte[] getPieceToReceivedBlocks() { + return pieceToReceivedBlocks; + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(verifiedPieces); + result = 31 * result + Arrays.hashCode(pieceToReceivedBlocks); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TorrentProgressComponent that = (TorrentProgressComponent) o; + return Arrays.equals(verifiedPieces, that.verifiedPieces) + && Arrays.equals(pieceToReceivedBlocks, that.pieceToReceivedBlocks); + } + + @Override + public String toString() { + return "TorrentProgressComponent{" + + ", pieceToReceivedBlocks=" + Arrays.toString(pieceToReceivedBlocks) + + ", verifiedPieces=" + Arrays.toString(verifiedPieces) + + '}'; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/model/TorrentStatisticsComponent.java b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentStatisticsComponent.java new file mode 100644 index 00000000..def06224 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/model/TorrentStatisticsComponent.java @@ -0,0 +1,66 @@ +package jtorrent.data.torrent.source.db.model; + +import jakarta.persistence.Column; +import jtorrent.domain.torrent.model.TorrentStatistics; + +public class TorrentStatisticsComponent { + + @Column(nullable = false) + private final long downloaded; + + @Column(nullable = false) + private final long uploaded; + + protected TorrentStatisticsComponent() { + this(0, 0); + } + + public TorrentStatisticsComponent(long downloaded, long uploaded) { + this.downloaded = downloaded; + this.uploaded = uploaded; + } + + public static TorrentStatisticsComponent fromDomain(TorrentStatistics torrentStatistics) { + return new TorrentStatisticsComponent(torrentStatistics.getDownloaded(), torrentStatistics.getUploaded()); + } + + public TorrentStatistics toDomain() { + return new TorrentStatistics(downloaded, uploaded); + } + + public long getDownloaded() { + return downloaded; + } + + public long getUploaded() { + return uploaded; + } + + @Override + public int hashCode() { + int result = Long.hashCode(downloaded); + result = 31 * result + Long.hashCode(uploaded); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TorrentStatisticsComponent that = (TorrentStatisticsComponent) o; + return downloaded == that.downloaded && uploaded == that.uploaded; + } + + @Override + public String toString() { + return "TorrentStatisticsComponent{" + + "downloaded=" + downloaded + + ", uploaded=" + uploaded + + '}'; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/db/util/HibernateUtil.java b/src/main/java/jtorrent/data/torrent/source/db/util/HibernateUtil.java new file mode 100644 index 00000000..9077add8 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/db/util/HibernateUtil.java @@ -0,0 +1,27 @@ +package jtorrent.data.torrent.source.db.util; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; + +import jtorrent.data.torrent.source.db.model.TorrentEntity; + +public class HibernateUtil { + + private static SessionFactory sessionFactory; + + private HibernateUtil() { + } + + public static synchronized SessionFactory getSessionFactory() { + if (sessionFactory == null) { + StandardServiceRegistry registry = new StandardServiceRegistryBuilder().build(); + sessionFactory = new MetadataSources(registry) + .addAnnotatedClass(TorrentEntity.class) + .buildMetadata() + .buildSessionFactory(); + } + return sessionFactory; + } +} diff --git a/src/main/java/jtorrent/data/torrent/source/file/filemanager/BencodedTorrentFileManager.java b/src/main/java/jtorrent/data/torrent/source/file/filemanager/BencodedTorrentFileManager.java new file mode 100644 index 00000000..c65d2407 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/file/filemanager/BencodedTorrentFileManager.java @@ -0,0 +1,68 @@ +package jtorrent.data.torrent.source.file.filemanager; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +import jtorrent.data.torrent.source.file.model.BencodedTorrent; + +public class BencodedTorrentFileManager { + + /** + * Reads a torrent file from the given URL. + * + * @param url the URL where the torrent file is located + * @return the bencoded torrent read from the file + * @throws IOException if an error occurs while reading the file + */ + public BencodedTorrent read(URL url) throws IOException { + // For some reason decoding directly from the URL stream doesn't work, so we have to read it into a byte array + // first. + try (BufferedInputStream in = new BufferedInputStream(url.openStream()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ) { + byte[] dataBuffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { + out.write(dataBuffer, 0, bytesRead); + } + + try (InputStream inputStream = new ByteArrayInputStream(out.toByteArray())) { + return BencodedTorrent.decode(inputStream); + } + } + } + + /** + * Reads a torrent file from the given file. + * + * @param file the file to read the torrent from + * @return the bencoded torrent read from the file + * @throws IOException if an error occurs while reading the file + */ + public BencodedTorrent read(File file) throws IOException { + InputStream inputStream = new FileInputStream(file); + return BencodedTorrent.decode(inputStream); + } + + /** + * Creates a new torrent file at the given path with the given bencoded torrent. + * + * @param path the path to create the torrent file at + * @param bencodedTorrent the bencoded torrent to write + * @throws IOException if an error occurs while writing the file + */ + public void write(Path path, BencodedTorrent bencodedTorrent) throws IOException { + try (OutputStream outputStream = Files.newOutputStream(path)) { + outputStream.write(bencodedTorrent.bencode()); + } + } +} diff --git a/src/main/java/jtorrent/data/torrent/model/BencodedFile.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedFile.java similarity index 96% rename from src/main/java/jtorrent/data/torrent/model/BencodedFile.java rename to src/main/java/jtorrent/data/torrent/source/file/model/BencodedFile.java index 77e66ad2..2f75c19e 100644 --- a/src/main/java/jtorrent/data/torrent/model/BencodedFile.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedFile.java @@ -1,4 +1,4 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -8,7 +8,7 @@ import java.util.Objects; import java.util.stream.Collectors; -import jtorrent.data.torrent.model.util.MapUtil; +import jtorrent.data.torrent.source.file.model.util.MapUtil; import jtorrent.domain.common.util.bencode.BencodedObject; import jtorrent.domain.torrent.model.FileMetadata; diff --git a/src/main/java/jtorrent/data/torrent/model/BencodedInfo.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedInfo.java similarity index 82% rename from src/main/java/jtorrent/data/torrent/model/BencodedInfo.java rename to src/main/java/jtorrent/data/torrent/source/file/model/BencodedInfo.java index 224c0de4..b5c44f8b 100644 --- a/src/main/java/jtorrent/data/torrent/model/BencodedInfo.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedInfo.java @@ -1,8 +1,7 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -32,16 +31,6 @@ protected BencodedInfo(int pieceLength, byte[] pieces, String name) { this.name = name; } - protected List getDomainPieceHashes() { - List pieceHashes = new ArrayList<>(); - for (int i = 0; i < pieces.length; i += Sha1Hash.HASH_SIZE) { - byte[] pieceHash = new byte[Sha1Hash.HASH_SIZE]; - System.arraycopy(pieces, i, pieceHash, 0, Sha1Hash.HASH_SIZE); - pieceHashes.add(new Sha1Hash(pieceHash)); - } - return pieceHashes; - } - public int getNumPieces() { return pieces.length / Sha1Hash.HASH_SIZE; } diff --git a/src/main/java/jtorrent/data/torrent/model/BencodedInfoFactory.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedInfoFactory.java similarity index 81% rename from src/main/java/jtorrent/data/torrent/model/BencodedInfoFactory.java rename to src/main/java/jtorrent/data/torrent/source/file/model/BencodedInfoFactory.java index 7961a4a4..d826c232 100644 --- a/src/main/java/jtorrent/data/torrent/model/BencodedInfoFactory.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedInfoFactory.java @@ -1,7 +1,7 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; -import static jtorrent.data.torrent.model.BencodedInfo.KEY_FILES; -import static jtorrent.data.torrent.model.BencodedInfo.KEY_LENGTH; +import static jtorrent.data.torrent.source.file.model.BencodedInfo.KEY_FILES; +import static jtorrent.data.torrent.source.file.model.BencodedInfo.KEY_LENGTH; import java.util.Map; diff --git a/src/main/java/jtorrent/data/torrent/model/BencodedMultiFileInfo.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedMultiFileInfo.java similarity index 94% rename from src/main/java/jtorrent/data/torrent/model/BencodedMultiFileInfo.java rename to src/main/java/jtorrent/data/torrent/source/file/model/BencodedMultiFileInfo.java index 4834c2b2..c381946a 100644 --- a/src/main/java/jtorrent/data/torrent/model/BencodedMultiFileInfo.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedMultiFileInfo.java @@ -1,4 +1,4 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -9,7 +9,7 @@ import java.util.Objects; import java.util.stream.Collectors; -import jtorrent.data.torrent.model.util.MapUtil; +import jtorrent.data.torrent.source.file.model.util.MapUtil; import jtorrent.domain.common.util.Sha1Hash; import jtorrent.domain.torrent.model.FileInfo; import jtorrent.domain.torrent.model.FileMetadata; @@ -61,7 +61,8 @@ public long getTotalSize() { @Override public FileInfo toDomain() { List fileMetaData = buildFileMetaData(); - return new MultiFileInfo(name, fileMetaData, pieceLength, getDomainPieceHashes(), new Sha1Hash(getInfoHash())); + return new MultiFileInfo(name, fileMetaData, pieceLength, Sha1Hash.splitHashes(pieces), + new Sha1Hash(getInfoHash())); } protected List buildFileMetaData() { @@ -93,8 +94,8 @@ protected List buildFileMetaData() { prevLastPieceEnd = lastPieceEnd; Path filePath = Path.of(String.join("/", sanitizePath(file.getPath()))); - FileMetadata fileMetadataItem = new FileMetadata(fileSize, filePath, firstPiece, - firstPieceStart, lastPiece, lastPieceEnd, fileStart, fileEnd); + FileMetadata fileMetadataItem = new FileMetadata(filePath, fileStart, fileSize, firstPiece, + firstPieceStart, lastPiece, lastPieceEnd); fileMetaData.add(fileMetadataItem); } diff --git a/src/main/java/jtorrent/data/torrent/model/BencodedSingleFileInfo.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedSingleFileInfo.java similarity index 90% rename from src/main/java/jtorrent/data/torrent/model/BencodedSingleFileInfo.java rename to src/main/java/jtorrent/data/torrent/source/file/model/BencodedSingleFileInfo.java index 607bc363..5fed63f9 100644 --- a/src/main/java/jtorrent/data/torrent/model/BencodedSingleFileInfo.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedSingleFileInfo.java @@ -1,4 +1,4 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -7,7 +7,7 @@ import java.util.Map; import java.util.Objects; -import jtorrent.data.torrent.model.util.MapUtil; +import jtorrent.data.torrent.source.file.model.util.MapUtil; import jtorrent.domain.common.util.Sha1Hash; import jtorrent.domain.torrent.model.FileInfo; import jtorrent.domain.torrent.model.FileMetadata; @@ -51,7 +51,7 @@ public long getTotalSize() { @Override public FileInfo toDomain() { FileMetadata fileMetaData = buildFileMetaData(); - return new SingleFileInfo(fileMetaData, pieceLength, getDomainPieceHashes(), new Sha1Hash(getInfoHash())); + return new SingleFileInfo(fileMetaData, pieceLength, Sha1Hash.splitHashes(pieces), new Sha1Hash(getInfoHash())); } private FileMetadata buildFileMetaData() { @@ -60,8 +60,7 @@ private FileMetadata buildFileMetaData() { long fileEnd = length - 1; int lastPieceEnd = (int) (fileEnd % pieceLength); - return new FileMetadata(length, filePath, 0, 0, - lastPiece, lastPieceEnd, 0, fileEnd); + return new FileMetadata(filePath, 0, length, 0, 0, lastPiece, lastPieceEnd); } @Override diff --git a/src/main/java/jtorrent/data/torrent/model/BencodedTorrent.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedTorrent.java similarity index 86% rename from src/main/java/jtorrent/data/torrent/model/BencodedTorrent.java rename to src/main/java/jtorrent/data/torrent/source/file/model/BencodedTorrent.java index 4c3f1d42..e5b4ae71 100644 --- a/src/main/java/jtorrent/data/torrent/model/BencodedTorrent.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedTorrent.java @@ -1,9 +1,9 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; -import static jtorrent.data.torrent.model.util.MapUtil.getValueAsList; -import static jtorrent.data.torrent.model.util.MapUtil.getValueAsLong; -import static jtorrent.data.torrent.model.util.MapUtil.getValueAsMap; -import static jtorrent.data.torrent.model.util.MapUtil.getValueAsString; +import static jtorrent.data.torrent.source.file.model.util.MapUtil.getValueAsList; +import static jtorrent.data.torrent.source.file.model.util.MapUtil.getValueAsLong; +import static jtorrent.data.torrent.source.file.model.util.MapUtil.getValueAsMap; +import static jtorrent.data.torrent.source.file.model.util.MapUtil.getValueAsString; import java.io.IOException; import java.io.InputStream; @@ -13,16 +13,14 @@ import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; import com.dampcake.bencode.BencodeInputStream; -import jtorrent.data.torrent.model.exception.MappingException; +import jtorrent.data.torrent.source.file.model.exception.MappingException; import jtorrent.domain.common.util.bencode.BencodedObject; import jtorrent.domain.torrent.model.FileInfo; import jtorrent.domain.torrent.model.TorrentMetadata; @@ -103,12 +101,13 @@ public static BencodedTorrent fromMap(Map map) { public static BencodedTorrent fromDomain(TorrentMetadata torrentMetadata) { Long creationDate = torrentMetadata.creationDate().toEpochSecond(OffsetDateTime.now().getOffset()); // TODO: proper handling of tracker groups - String announce = torrentMetadata.trackers().iterator().next().toString(); - List> announceList = - List.of(torrentMetadata.trackers().stream() + String announce = torrentMetadata.trackerTiers().get(0).get(0).toString(); + List> announceList = torrentMetadata.trackerTiers().stream() + .map(tier -> tier.stream() .map(URI::toString) .toList() - ); + ) + .toList(); BencodedInfo info = BencodedInfoFactory.fromDomain(torrentMetadata.fileInfo()); return new BencodedTorrent(creationDate, announce, announceList, torrentMetadata.comment(), torrentMetadata.createdBy(), info); @@ -152,14 +151,16 @@ public Collection getFiles() { public TorrentMetadata toDomain() { try { - Set trackers = new HashSet<>(); - trackers.add(URI.create(announce)); - - if (announceList != null) { - announceList.stream() - .flatMap(List::stream) - .map(URI::create) - .collect(Collectors.toCollection(() -> trackers)); + final List> trackers; + + if (announceList != null && !announceList.isEmpty()) { + trackers = announceList.stream() + .map(tier -> tier.stream() + .map(URI::create) + .toList() + ).toList(); + } else { + trackers = List.of(List.of(URI.create(announce))); } LocalDateTime creationDateTime = LocalDateTime.ofEpochSecond(creationDate, 0, diff --git a/src/main/java/jtorrent/data/torrent/source/file/model/BencodedTorrentFactory.java b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedTorrentFactory.java new file mode 100644 index 00000000..e96be872 --- /dev/null +++ b/src/main/java/jtorrent/data/torrent/source/file/model/BencodedTorrentFactory.java @@ -0,0 +1,84 @@ +package jtorrent.data.torrent.source.file.model; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import jtorrent.domain.common.util.ContinuousMergedInputStream; +import jtorrent.domain.common.util.Sha1Hash; + +public class BencodedTorrentFactory { + + private BencodedTorrentFactory() { + } + + public static BencodedTorrent create(Path source, List> trackerUrls, String comment, + String createdBy, int pieceSize) throws IOException { + Long creationDate = LocalDateTime.now().toEpochSecond(OffsetDateTime.now().getOffset()); + BencodedInfo info = buildBencodedInfo(source, pieceSize); + return BencodedTorrent.withAnnounceList(creationDate, trackerUrls, comment, createdBy, info); + } + + private static BencodedInfo buildBencodedInfo(Path source, int pieceSize) throws IOException { + if (Files.isDirectory(source)) { + return buildBencodedMultiFileInfo(source, pieceSize); + } else { + return buildBencodedSingleFileInfo(source, pieceSize); + } + } + + private static BencodedSingleFileInfo buildBencodedSingleFileInfo(Path source, int pieceSize) throws IOException { + if (Files.isDirectory(source)) { + throw new IllegalArgumentException("Source must be a file"); + } + + byte[] hashes = computeHashes(Files.newInputStream(source), pieceSize); + String fileName = source.getFileName().toString(); + long length = Files.size(source); + return new BencodedSingleFileInfo(pieceSize, hashes, fileName, length); + } + + private static BencodedMultiFileInfo buildBencodedMultiFileInfo(Path source, int pieceSize) throws IOException { + if (!Files.isDirectory(source)) { + throw new IllegalArgumentException("Source must be a directory"); + } + + List filePaths = getFilesInDirectory(source); + + List inputStreams = new ArrayList<>(); + List files = new ArrayList<>(); + for (Path filePath : filePaths) { + long length = Files.size(filePath); + files.add(BencodedFile.fromPath(source.relativize(filePath), length)); + inputStreams.add(Files.newInputStream(filePath)); + } + + byte[] hashes = computeHashes(new ContinuousMergedInputStream(inputStreams), pieceSize); + String dirName = source.getFileName().toString(); + return new BencodedMultiFileInfo(pieceSize, hashes, dirName, files); + } + + private static List getFilesInDirectory(Path directory) throws IOException { + try (Stream stream = Files.walk(directory)) { + return stream.filter(Files::isRegularFile).toList(); + } + } + + private static byte[] computeHashes(InputStream inputStream, int pieceSize) throws IOException { + List hashes = new ArrayList<>(); + int bytesRead; + byte[] buffer = new byte[pieceSize]; + while ((bytesRead = inputStream.read(buffer)) != -1) { + byte[] piece = Arrays.copyOf(buffer, bytesRead); + hashes.add(Sha1Hash.of(piece)); + } + return Sha1Hash.concatHashes(hashes); + } +} diff --git a/src/main/java/jtorrent/data/torrent/model/exception/MappingException.java b/src/main/java/jtorrent/data/torrent/source/file/model/exception/MappingException.java similarity index 80% rename from src/main/java/jtorrent/data/torrent/model/exception/MappingException.java rename to src/main/java/jtorrent/data/torrent/source/file/model/exception/MappingException.java index e05b8372..cc52544f 100644 --- a/src/main/java/jtorrent/data/torrent/model/exception/MappingException.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/exception/MappingException.java @@ -1,4 +1,4 @@ -package jtorrent.data.torrent.model.exception; +package jtorrent.data.torrent.source.file.model.exception; public class MappingException extends RuntimeException { diff --git a/src/main/java/jtorrent/data/torrent/model/util/MapUtil.java b/src/main/java/jtorrent/data/torrent/source/file/model/util/MapUtil.java similarity index 95% rename from src/main/java/jtorrent/data/torrent/model/util/MapUtil.java rename to src/main/java/jtorrent/data/torrent/source/file/model/util/MapUtil.java index 55bf3f3d..b748d78b 100644 --- a/src/main/java/jtorrent/data/torrent/model/util/MapUtil.java +++ b/src/main/java/jtorrent/data/torrent/source/file/model/util/MapUtil.java @@ -1,4 +1,4 @@ -package jtorrent.data.torrent.model.util; +package jtorrent.data.torrent.source.file.model.util; import java.nio.ByteBuffer; import java.util.Collections; diff --git a/src/main/java/jtorrent/domain/Client.java b/src/main/java/jtorrent/domain/Client.java index e0b467db..eef5f127 100644 --- a/src/main/java/jtorrent/domain/Client.java +++ b/src/main/java/jtorrent/domain/Client.java @@ -29,6 +29,7 @@ import jtorrent.domain.torrent.model.Torrent; import jtorrent.domain.torrent.model.TorrentMetadata; import jtorrent.domain.torrent.repository.PieceRepository; +import jtorrent.domain.torrent.repository.TorrentMetadataRepository; import jtorrent.domain.torrent.repository.TorrentRepository; public class Client implements LocalServiceDiscoveryManager.Listener, TorrentHandler.Listener, @@ -41,13 +42,15 @@ public class Client implements LocalServiceDiscoveryManager.Listener, TorrentHan private final DhtClient dhtManager; private final Map infoHashToTorrentHandler = new HashMap<>(); private final TorrentRepository torrentRepository; + private final TorrentMetadataRepository torrentMetadataRepository; private final PieceRepository pieceRepository; private final HandleInboundConnectionsTask handleInboundConnectionsTask = new HandleInboundConnectionsTask(); - public Client(TorrentRepository torrentRepository, PieceRepository pieceRepository, - InboundConnectionListener inboundConnectionListener, + public Client(TorrentRepository torrentRepository, TorrentMetadataRepository torrentMetadataRepository, + PieceRepository pieceRepository, InboundConnectionListener inboundConnectionListener, LocalServiceDiscoveryManager localServiceDiscoveryManager, DhtClient dhtClient) { this.torrentRepository = torrentRepository; + this.torrentMetadataRepository = torrentMetadataRepository; this.pieceRepository = pieceRepository; this.inboundConnectionListener = inboundConnectionListener; @@ -85,10 +88,11 @@ public void shutdown() { localServiceDiscoveryManager.stop(); dhtManager.stop(); infoHashToTorrentHandler.values().forEach(TorrentHandler::stop); + torrentRepository.persistTorrents(); } public void addTorrent(TorrentMetadata torrentMetaData, String name, Path saveDirectory) { - Torrent torrent = new Torrent(torrentMetaData, name, saveDirectory); + Torrent torrent = Torrent.createNew(torrentMetaData, name, saveDirectory); torrentRepository.addTorrent(torrent); } @@ -96,12 +100,12 @@ public void removeTorrent(Torrent torrent) { torrentRepository.removeTorrent(torrent); } - public TorrentMetadata loadTorrent(File file) throws IOException { - return torrentRepository.loadTorrent(file); + public TorrentMetadata loadTorrentMetadata(File file) throws IOException { + return torrentMetadataRepository.getTorrentMetadata(file); } - public TorrentMetadata loadTorrent(URL url) throws IOException { - return torrentRepository.loadTorrent(url); + public TorrentMetadata loadTorrentMetadata(URL url) throws IOException { + return torrentMetadataRepository.getTorrentMetadata(url); } public void startTorrent(Torrent torrent) { @@ -179,9 +183,9 @@ public double getUploadRate() { public void createNewTorrent(Path savePath, Path source, List> trackerUrls, String comment, int pieceSize) throws IOException { - TorrentMetadata torrentMetadata = torrentRepository.createNewTorrent(source, trackerUrls, comment, + TorrentMetadata torrentMetadata = torrentMetadataRepository.createTOrrentMetadata(source, trackerUrls, comment, "JTorrent", pieceSize); - torrentRepository.saveTorrent(torrentMetadata, savePath); + torrentMetadataRepository.saveTorrentMetadata(torrentMetadata, savePath); } public Torrent getTorrent(Sha1Hash infoHash) { diff --git a/src/main/java/jtorrent/domain/common/util/Sha1Hash.java b/src/main/java/jtorrent/domain/common/util/Sha1Hash.java index e46ccacf..395f420b 100644 --- a/src/main/java/jtorrent/domain/common/util/Sha1Hash.java +++ b/src/main/java/jtorrent/domain/common/util/Sha1Hash.java @@ -2,6 +2,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.List; public class Sha1Hash extends Bit160Value { @@ -49,6 +50,20 @@ public static Sha1Hash fromHexString(String hexString) { return new Sha1Hash(hash); } + public static List splitHashes(byte[] hashesConcat) { + if (hashesConcat.length % HASH_SIZE != 0) { + throw new IllegalArgumentException("Invalid concatenated hashes length"); + } + + List pieceHashes = new ArrayList<>(); + for (int i = 0; i < hashesConcat.length; i += Sha1Hash.HASH_SIZE) { + byte[] pieceHash = new byte[Sha1Hash.HASH_SIZE]; + System.arraycopy(hashesConcat, i, pieceHash, 0, Sha1Hash.HASH_SIZE); + pieceHashes.add(new Sha1Hash(pieceHash)); + } + return pieceHashes; + } + public static byte[] concatHashes(List hashes) { byte[] hashesConcat = new byte[hashes.size() * HASH_SIZE]; for (int i = 0; i < hashes.size(); i++) { diff --git a/src/main/java/jtorrent/domain/common/util/rx/RxObservableCollectionBase.java b/src/main/java/jtorrent/domain/common/util/rx/RxObservableCollectionBase.java index 5dff745f..166475a2 100644 --- a/src/main/java/jtorrent/domain/common/util/rx/RxObservableCollectionBase.java +++ b/src/main/java/jtorrent/domain/common/util/rx/RxObservableCollectionBase.java @@ -95,4 +95,9 @@ public boolean equals(Object o) { RxObservableCollectionBase that = (RxObservableCollectionBase) o; return Objects.equals(collection, that.collection); } + + @Override + public String toString() { + return collection.toString(); + } } diff --git a/src/main/java/jtorrent/domain/torrent/handler/TorrentHandler.java b/src/main/java/jtorrent/domain/torrent/handler/TorrentHandler.java index f083e7c7..f81d7d8a 100644 --- a/src/main/java/jtorrent/domain/torrent/handler/TorrentHandler.java +++ b/src/main/java/jtorrent/domain/torrent/handler/TorrentHandler.java @@ -127,6 +127,7 @@ private void verifyFiles() { torrent.resetCheckedBytes(); synchronized (pieceStateLock) { IntStream.range(0, torrent.getNumPieces()) + .parallel() .forEach(piece -> { if (isPieceChecksumValid(piece)) { torrent.setPieceVerified(piece); diff --git a/src/main/java/jtorrent/domain/torrent/model/FileInfo.java b/src/main/java/jtorrent/domain/torrent/model/FileInfo.java index 304b0962..776a445e 100644 --- a/src/main/java/jtorrent/domain/torrent/model/FileInfo.java +++ b/src/main/java/jtorrent/domain/torrent/model/FileInfo.java @@ -18,7 +18,7 @@ public abstract class FileInfo { protected final List fileMetaData; protected final List pieceHashes; protected final int pieceSize; - private final Sha1Hash infoHash; + protected final Sha1Hash infoHash; protected FileInfo(List fileMetaData, int pieceSize, List pieceHashes, Sha1Hash infoHash) { this.fileMetaData = requireNonNull(fileMetaData); @@ -87,6 +87,10 @@ public int getNumBlocks(int pieceIndex) { } public int getPieceSize(int piece) { + if (piece < 0 || piece >= getNumPieces()) { + throw new IllegalArgumentException("Invalid piece index: " + piece); + } + if (piece == getNumPieces() - 1) { int remainder = (int) (getTotalFileSize() % pieceSize); return remainder == 0 ? pieceSize : remainder; @@ -117,6 +121,13 @@ public List getFileMetaData() { return fileMetaData; } + public FileMetadata getFileMetaData(Path path) { + return fileMetaData.stream() + .filter(file -> file.path().equals(path)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("File not found: " + path)); + } + public List getPieceHashes() { return pieceHashes; } diff --git a/src/main/java/jtorrent/domain/torrent/model/FileMetadata.java b/src/main/java/jtorrent/domain/torrent/model/FileMetadata.java index 0d563a3b..eb3fb87e 100644 --- a/src/main/java/jtorrent/domain/torrent/model/FileMetadata.java +++ b/src/main/java/jtorrent/domain/torrent/model/FileMetadata.java @@ -2,10 +2,14 @@ import java.nio.file.Path; -public record FileMetadata(long size, Path path, int firstPiece, int firstPieceStart, int lastPiece, int lastPieceEnd, - long start, long end) { +public record FileMetadata(Path path, long start, long size, int firstPiece, int firstPieceStart, int lastPiece, + int lastPieceEnd) { public int numPieces() { return lastPiece - firstPiece + 1; } + + public long end() { + return start + size - 1; + } } diff --git a/src/main/java/jtorrent/domain/torrent/model/FileProgress.java b/src/main/java/jtorrent/domain/torrent/model/FileProgress.java index ffddb055..0cabd627 100644 --- a/src/main/java/jtorrent/domain/torrent/model/FileProgress.java +++ b/src/main/java/jtorrent/domain/torrent/model/FileProgress.java @@ -1,7 +1,10 @@ package jtorrent.domain.torrent.model; +import static jtorrent.domain.common.util.ValidationUtil.requireNonNull; + import java.util.BitSet; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.subjects.BehaviorSubject; @@ -10,18 +13,61 @@ public class FileProgress { private final FileInfo fileInfo; private final FileMetadata fileMetaData; - private final AtomicLong verifiedBytes = new AtomicLong(0L); - private final BehaviorSubject verifiedBytesSubject = BehaviorSubject.createDefault(0L); - private final BitSet verifiedPieces = new BitSet(); - private final BehaviorSubject verifiedPiecesSubject = BehaviorSubject.createDefault(new BitSet()); + private final AtomicLong verifiedBytes; + private final BehaviorSubject verifiedBytesSubject; + private final BitSet verifiedPieces; + private final BehaviorSubject verifiedPiecesSubject; - public FileProgress(FileInfo fileInfo, FileMetadata fileMetaData) { - this.fileInfo = fileInfo; - this.fileMetaData = fileMetaData; + public FileProgress(FileInfo fileInfo, FileMetadata fileMetaData, long verifiedBytes, BitSet verifiedPieces) { + this.fileInfo = requireNonNull(fileInfo); + this.fileMetaData = requireNonNull(fileMetaData); + this.verifiedBytes = new AtomicLong(verifiedBytes); + this.verifiedBytesSubject = BehaviorSubject.createDefault(verifiedBytes); + this.verifiedPieces = requireNonNull(verifiedPieces); + this.verifiedPiecesSubject = BehaviorSubject.createDefault((BitSet) verifiedPieces.clone()); } - private void incrementVerifiedBytes(long bytes) { - verifiedBytesSubject.onNext(verifiedBytes.addAndGet(bytes)); + public static FileProgress createNew(FileInfo fileInfo, FileMetadata fileMetaData) { + return new FileProgress(fileInfo, fileMetaData, 0, new BitSet()); + } + + /** + * @param fileInfo the file info for the torrent + * @param fileMetaData the metadata for the file + * @param verifiedPieces a bitset containing the verified pieces indices for the entire torrent, i.e., + * indices are global, not relative to the file + */ + public static FileProgress createExisting(FileInfo fileInfo, FileMetadata fileMetaData, + BitSet verifiedPieces) { + long verifiedBytes = IntStream.range(fileMetaData.firstPiece(), fileMetaData.lastPiece() + 1) + .filter(verifiedPieces::get) + .mapToLong(piece -> getPieceBytesInFile(fileInfo, fileMetaData, piece)) + .sum(); + BitSet relativeVerifiedPieces = new BitSet(); + IntStream.range(fileMetaData.firstPiece(), fileMetaData.lastPiece() + 1) + .filter(verifiedPieces::get) + .forEach(piece -> relativeVerifiedPieces.set(piece - fileMetaData.firstPiece())); + return new FileProgress(fileInfo, fileMetaData, verifiedBytes, relativeVerifiedPieces); + } + + private static long getPieceBytesInFile(FileInfo fileInfo, FileMetadata fileMetaData, int piece) { + long pieceStart = fileInfo.getPieceOffset(piece); + long pieceEnd = pieceStart + fileInfo.getPieceSize(piece) - 1; + long pieceStartWithinFile = Math.max(pieceStart, fileMetaData.start()); + long pieceEndWithinFile = Math.min(pieceEnd, fileMetaData.end()); + return pieceEndWithinFile - pieceStartWithinFile + 1; + } + + private long getPieceBytesInFile(int piece) { + return getPieceBytesInFile(fileInfo, fileMetaData, piece); + } + + public long getVerifiedBytes() { + return verifiedBytes.get(); + } + + public BitSet getVerifiedPieces() { + return verifiedPieces; } public Observable getVerifiedBytesObservable() { @@ -42,6 +88,14 @@ public void setPieceVerified(int piece) { incrementVerifiedBytes(getPieceBytesInFile(piece)); } + private void incrementVerifiedBytes(long bytes) { + verifiedBytesSubject.onNext(verifiedBytes.addAndGet(bytes)); + } + + private int getRelativePieceIndex(int piece) { + return piece - fileMetaData.firstPiece(); + } + public void setPieceNotVerified(int piece) { int relativePiece = getRelativePieceIndex(piece); if (!verifiedPieces.get(relativePiece)) { @@ -52,15 +106,38 @@ public void setPieceNotVerified(int piece) { incrementVerifiedBytes(-getPieceBytesInFile(piece)); } - private long getPieceBytesInFile(int piece) { - long pieceStart = fileInfo.getPieceOffset(piece); - long pieceEnd = pieceStart + fileInfo.getPieceSize(piece) - 1; - long pieceStartWithinFile = Math.max(pieceStart, fileMetaData.start()); - long pieceEndWithinFile = Math.min(pieceEnd, fileMetaData.end()); - return pieceEndWithinFile - pieceStartWithinFile + 1; + @Override + public int hashCode() { + int result = fileInfo.hashCode(); + result = 31 * result + fileMetaData.hashCode(); + result = 31 * result + verifiedBytes.hashCode(); + result = 31 * result + verifiedPieces.hashCode(); + return result; } - private int getRelativePieceIndex(int piece) { - return piece - fileMetaData.firstPiece(); + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FileProgress that = (FileProgress) o; + return fileInfo.equals(that.fileInfo) + && fileMetaData.equals(that.fileMetaData) + && (verifiedBytes.get() == that.verifiedBytes.get()) + && verifiedPieces.equals(that.verifiedPieces); + } + + @Override + public String toString() { + return "FileProgress{" + + "fileInfo=" + fileInfo + + ", fileMetaData=" + fileMetaData + + ", verifiedBytes=" + verifiedBytes + + ", verifiedPieces=" + verifiedPieces + + '}'; } } diff --git a/src/main/java/jtorrent/domain/torrent/model/MultiFileInfo.java b/src/main/java/jtorrent/domain/torrent/model/MultiFileInfo.java index deef7626..36c1c868 100644 --- a/src/main/java/jtorrent/domain/torrent/model/MultiFileInfo.java +++ b/src/main/java/jtorrent/domain/torrent/model/MultiFileInfo.java @@ -25,6 +25,10 @@ public Path getFileRoot() { @Override public String getName() { + return getDirectory(); + } + + public String getDirectory() { return directory; } @@ -52,9 +56,10 @@ public boolean equals(Object o) { public String toString() { return "MultiFileInfo{" + "directory='" + directory + '\'' - + ", fileWithPieceInfos=" + fileMetaData + + ", fileMetaData=" + fileMetaData + ", pieceHashes=" + pieceHashes + ", pieceSize=" + pieceSize + + ", infoHash=" + infoHash + '}'; } } diff --git a/src/main/java/jtorrent/domain/torrent/model/SingleFileInfo.java b/src/main/java/jtorrent/domain/torrent/model/SingleFileInfo.java index c7af2eea..a4a8a406 100644 --- a/src/main/java/jtorrent/domain/torrent/model/SingleFileInfo.java +++ b/src/main/java/jtorrent/domain/torrent/model/SingleFileInfo.java @@ -24,9 +24,10 @@ public String getName() { @Override public String toString() { return "SingleFileInfo{" - + "fileWithPieceInfos=" + fileMetaData + + "fileMetaData=" + fileMetaData + ", pieceHashes=" + pieceHashes + ", pieceSize=" + pieceSize - + "} "; + + ", infoHash=" + infoHash + + '}'; } } diff --git a/src/main/java/jtorrent/domain/torrent/model/Torrent.java b/src/main/java/jtorrent/domain/torrent/model/Torrent.java index 3b95fb6a..c9f593a1 100644 --- a/src/main/java/jtorrent/domain/torrent/model/Torrent.java +++ b/src/main/java/jtorrent/domain/torrent/model/Torrent.java @@ -3,11 +3,11 @@ import static jtorrent.domain.common.util.ValidationUtil.requireNonNull; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.LocalDateTime; import java.util.BitSet; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -26,7 +26,7 @@ public class Torrent implements TrackerHandler.TorrentProgressProvider { private final TorrentMetadata torrentMetaData; - private final TorrentStatistics torrentStatistics = new TorrentStatistics(); + private final TorrentStatistics torrentStatistics; private final TorrentProgress torrentProgress; private final Set trackers = new HashSet<>(); private final MutableRxObservableSet peers = new MutableRxObservableSet<>(new HashSet<>()); @@ -36,24 +36,30 @@ public class Torrent implements TrackerHandler.TorrentProgressProvider { private String name; private Path saveDirectory; - private State state = State.STOPPED; - private final BehaviorSubject stateSubject = BehaviorSubject.createDefault(state); + private State state; + private final BehaviorSubject stateSubject; - public Torrent(TorrentMetadata torrentMetaData) { - // TODO: use default downloads folder? - this(torrentMetaData, torrentMetaData.fileInfo().getName(), Paths.get("download").toAbsolutePath()); - } - - public Torrent(TorrentMetadata torrentMetaData, String name, Path saveDirectory) { + public Torrent(TorrentMetadata torrentMetaData, TorrentStatistics torrentStatistics, + TorrentProgress torrentProgress, String name, Path saveDirectory, State state) { this.torrentMetaData = requireNonNull(torrentMetaData); - this.torrentProgress = new TorrentProgress(torrentMetaData.fileInfo()); + this.torrentStatistics = requireNonNull(torrentStatistics); + this.torrentProgress = requireNonNull(torrentProgress); this.name = name; - this.saveDirectory = saveDirectory; - torrentMetaData.trackers().stream() + this.saveDirectory = requireNonNull(saveDirectory); + this.state = requireNonNull(state); + this.stateSubject = BehaviorSubject.createDefault(state); + + torrentMetaData.trackerTiers().get(0).stream() .map(TrackerFactory::fromUri) .collect(Collectors.toCollection(() -> trackers)); } + public static Torrent createNew(TorrentMetadata torrentMetaData, String name, Path saveDirectory) { + TorrentStatistics torrentStatistics = TorrentStatistics.createNew(); + TorrentProgress torrentProgress = TorrentProgress.createNew(torrentMetaData.fileInfo()); + return new Torrent(torrentMetaData, torrentStatistics, torrentProgress, name, saveDirectory, State.STOPPED); + } + public Path getSaveDirectory() { return saveDirectory; } @@ -135,7 +141,7 @@ public List getFileMetaDataWithState() { return torrentMetaData.fileInfo().getFileMetaData() .stream() .map(fileMetaData -> new FileMetadataWithState(fileMetaData, - torrentProgress.getFileState(fileMetaData.path()))) + torrentProgress.getFileProgress(fileMetaData.path()))) .toList(); } @@ -318,11 +324,62 @@ public Observable getStateObservable() { return stateSubject; } + public TorrentMetadata getMetadata() { + return torrentMetaData; + } + + public TorrentStatistics getStatistics() { + return torrentStatistics; + } + + public TorrentProgress getProgress() { + return torrentProgress; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Torrent torrent = (Torrent) o; + return torrentMetaData.equals(torrent.torrentMetaData) + && torrentStatistics.equals(torrent.torrentStatistics) + && torrentProgress.equals(torrent.torrentProgress) + && trackers.equals(torrent.trackers) + && peers.equals(torrent.peers) + && Objects.equals(name, torrent.name) + && saveDirectory.equals(torrent.saveDirectory) + && state == torrent.state; + } + + @Override + public int hashCode() { + int result = torrentMetaData.hashCode(); + result = 31 * result + torrentStatistics.hashCode(); + result = 31 * result + torrentProgress.hashCode(); + result = 31 * result + trackers.hashCode(); + result = 31 * result + peers.hashCode(); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + saveDirectory.hashCode(); + result = 31 * result + state.hashCode(); + return result; + } + @Override public String toString() { return "Torrent{" - + "trackers=" + trackers + + "torrentMetaData=" + torrentMetaData + + ", torrentStatistics=" + torrentStatistics + + ", torrentProgress=" + torrentProgress + + ", trackers=" + trackers + + ", peers=" + peers + ", name='" + name + '\'' + + ", saveDirectory=" + saveDirectory + + ", state=" + state + '}'; } @@ -332,5 +389,4 @@ public enum State { DOWNLOADING, SEEDING } - } diff --git a/src/main/java/jtorrent/domain/torrent/model/TorrentMetadata.java b/src/main/java/jtorrent/domain/torrent/model/TorrentMetadata.java index fa95cce1..e5581036 100644 --- a/src/main/java/jtorrent/domain/torrent/model/TorrentMetadata.java +++ b/src/main/java/jtorrent/domain/torrent/model/TorrentMetadata.java @@ -2,8 +2,14 @@ import java.net.URI; import java.time.LocalDateTime; -import java.util.Set; +import java.util.List; -public record TorrentMetadata(Set trackers, LocalDateTime creationDate, String comment, String createdBy, - FileInfo fileInfo) { +public record TorrentMetadata(List> trackerTiers, LocalDateTime creationDate, String comment, + String createdBy, FileInfo fileInfo) { + + public TorrentMetadata { + if (trackerTiers.isEmpty() || trackerTiers.get(0).isEmpty()) { + throw new IllegalArgumentException("At least one tracker is required"); + } + } } diff --git a/src/main/java/jtorrent/domain/torrent/model/TorrentProgress.java b/src/main/java/jtorrent/domain/torrent/model/TorrentProgress.java index 211c175c..d650d571 100644 --- a/src/main/java/jtorrent/domain/torrent/model/TorrentProgress.java +++ b/src/main/java/jtorrent/domain/torrent/model/TorrentProgress.java @@ -16,41 +16,107 @@ public class TorrentProgress { private final FileInfo fileInfo; - private final Map pathToFileState = new HashMap<>(); - - private final AtomicLong verifiedBytes = new AtomicLong(0); - private final BehaviorSubject verifiedBytesSubject = BehaviorSubject.createDefault(0L); - private final BehaviorSubject checkedBytesSubject = BehaviorSubject.createDefault(0L); - private final BitSet completePieces = new BitSet(); - private final BitSet verifiedPieces = new BitSet(); - private final BehaviorSubject verifiedPiecesSubject = BehaviorSubject.createDefault(new BitSet()); + private final Map pathToFileProgress; + + private final AtomicLong verifiedBytes; + private final BehaviorSubject verifiedBytesSubject; + private final BehaviorSubject checkedBytesSubject; + private final BitSet completePieces; + private final BitSet verifiedPieces; + private final BehaviorSubject verifiedPiecesSubject; private final BehaviorSubject availablePiecesSubject = BehaviorSubject.createDefault(new BitSet()); private final Map pieceIndexToRequestedBlocks = new HashMap<>(); - private final Map pieceIndexToAvailableBlocks = new HashMap<>(); - private final BitSet partiallyMissingPieces = new BitSet(); - private final BitSet partiallyMissingPiecesWithUnrequestedBlocks = new BitSet(); - private final BitSet completelyMissingPieces = new BitSet(); - private final BitSet completelyMissingPiecesWithUnrequestedBlocks = new BitSet(); - private long checkedBytes = 0; - - public TorrentProgress(FileInfo fileInfo) { + private final Map pieceIndexToAvailableBlocks; + private final BitSet partiallyMissingPieces; + private final BitSet partiallyMissingPiecesWithUnrequestedBlocks; + private final BitSet completelyMissingPieces; + private final BitSet completelyMissingPiecesWithUnrequestedBlocks; + private long checkedBytes; + + public TorrentProgress(FileInfo fileInfo, Map pathToFileProgress, long verifiedBytes, + BitSet completePieces, BitSet verifiedPieces, Map pieceIndexToAvailableBlocks, + BitSet partiallyMissingPieces, BitSet partiallyMissingPiecesWithUnrequestedBlocks, + BitSet completelyMissingPieces, BitSet completelyMissingPiecesWithUnrequestedBlocks, long checkedBytes) { this.fileInfo = requireNonNull(fileInfo); + this.pathToFileProgress = pathToFileProgress; + this.verifiedBytes = new AtomicLong(verifiedBytes); + this.verifiedBytesSubject = BehaviorSubject.createDefault(verifiedBytes); + this.checkedBytesSubject = BehaviorSubject.createDefault(checkedBytes); + this.completePieces = completePieces; + this.verifiedPieces = verifiedPieces; + this.verifiedPiecesSubject = BehaviorSubject.createDefault((BitSet) verifiedPieces.clone()); + this.pieceIndexToAvailableBlocks = pieceIndexToAvailableBlocks; + this.partiallyMissingPieces = partiallyMissingPieces; + this.partiallyMissingPiecesWithUnrequestedBlocks = partiallyMissingPiecesWithUnrequestedBlocks; + this.completelyMissingPieces = completelyMissingPieces; + this.completelyMissingPiecesWithUnrequestedBlocks = completelyMissingPiecesWithUnrequestedBlocks; + this.checkedBytes = checkedBytes; + } + + public static TorrentProgress createNew(FileInfo fileInfo) { + Map pathToFileProgress = new HashMap<>(); + fileInfo.getFileMetaData() + .forEach(fileMetaData -> pathToFileProgress.put(fileMetaData.path(), + FileProgress.createNew(fileInfo, fileMetaData))); + BitSet completePieces = new BitSet(); + BitSet verifiedPieces = new BitSet(); + Map pieceIndexToAvailableBlocks = new HashMap<>(); + BitSet partiallyMissingPieces = new BitSet(); + BitSet partiallyMissingPiecesWithUnrequestedBlocks = new BitSet(); + BitSet completelyMissingPieces = new BitSet(); + completelyMissingPieces.set(0, fileInfo.getNumPieces()); + BitSet completelyMissingPiecesWithUnrequestedBlocks = new BitSet(); + completelyMissingPiecesWithUnrequestedBlocks.set(0, fileInfo.getNumPieces()); + return new TorrentProgress(fileInfo, pathToFileProgress, 0, completePieces, verifiedPieces, + pieceIndexToAvailableBlocks, partiallyMissingPieces, + partiallyMissingPiecesWithUnrequestedBlocks, completelyMissingPieces, + completelyMissingPiecesWithUnrequestedBlocks, 0); + } + + public static TorrentProgress createExisting(FileInfo fileInfo, Map pathToFileProgress, + BitSet verifiedPieces, Map pieceIndexToAvailableBlocks) { + + long verifiedBytes = verifiedPieces.stream() + .mapToLong(fileInfo::getPieceSize) + .sum(); + + BitSet completePieces = new BitSet(); + BitSet partiallyMissingPieces = new BitSet(); + BitSet completelyMissingPieces = new BitSet(); IntStream.range(0, fileInfo.getNumPieces()) - .forEach(i -> { - pieceIndexToRequestedBlocks.put(i, new BitSet()); - pieceIndexToAvailableBlocks.put(i, new BitSet()); - completelyMissingPieces.set(i); - completelyMissingPiecesWithUnrequestedBlocks.set(i); + .forEach(piece -> { + final int numAvailableBlocks; + if (pieceIndexToAvailableBlocks.containsKey(piece)) { + numAvailableBlocks = pieceIndexToAvailableBlocks.get(piece).cardinality(); + } else { + numAvailableBlocks = 0; + } + + if (numAvailableBlocks == fileInfo.getNumBlocks(piece)) { + completePieces.set(piece); + } else if (numAvailableBlocks > 0) { + partiallyMissingPieces.set(piece); + } else { + completelyMissingPieces.set(piece); + } }); - fileInfo.getFileMetaData() - .forEach(fileMetaData -> pathToFileState.put(fileMetaData.path(), - new FileProgress(fileInfo, fileMetaData))); + BitSet partiallyMissingPiecesWithUnrequestedBlocks = (BitSet) partiallyMissingPieces.clone(); + BitSet completelyMissingPiecesWithUnrequestedBlocks = (BitSet) completelyMissingPieces.clone(); + + return new TorrentProgress(fileInfo, pathToFileProgress, verifiedBytes, completePieces, verifiedPieces, + pieceIndexToAvailableBlocks, partiallyMissingPieces, + partiallyMissingPiecesWithUnrequestedBlocks, completelyMissingPieces, + completelyMissingPiecesWithUnrequestedBlocks, 0); } - public FileProgress getFileState(Path path) { - return pathToFileState.get(path); + public Map getFileProgress() { + return pathToFileProgress; + } + + public FileProgress getFileProgress(Path path) { + return pathToFileProgress.get(path); } public Observable getVerifiedBytesObservable() { @@ -95,7 +161,7 @@ public synchronized void setPieceVerified(int piece) { filesInRange.stream() .map(FileMetadata::path) - .map(pathToFileState::get) + .map(pathToFileProgress::get) .forEach(fileProgress -> fileProgress.setPieceVerified(piece)); } @@ -117,14 +183,14 @@ public synchronized void setPieceMissing(int piece) { filesInRange.stream() .map(FileMetadata::path) - .map(pathToFileState::get) + .map(pathToFileProgress::get) .forEach(fileProgress -> fileProgress.setPieceNotVerified(piece)); } verifiedPieces.clear(piece); verifiedPiecesSubject.onNext((BitSet) verifiedPieces.clone()); completePieces.clear(piece); - pieceIndexToAvailableBlocks.get(piece).clear(); + getAvailableBlocks(piece).clear(); partiallyMissingPieces.clear(piece); partiallyMissingPiecesWithUnrequestedBlocks.clear(piece); completelyMissingPieces.set(piece); @@ -155,8 +221,12 @@ private BitSet getUnavailableBlocks(int piece) { return unavailableBlocks; } + public Map getReceivedBlocks() { + return pieceIndexToAvailableBlocks; + } + private BitSet getAvailableBlocks(int piece) { - return pieceIndexToAvailableBlocks.get(piece); + return pieceIndexToAvailableBlocks.computeIfAbsent(piece, k -> new BitSet()); } private BitSet getUnrequestedBlocks(int piece) { @@ -167,11 +237,11 @@ private BitSet getUnrequestedBlocks(int piece) { } private BitSet getRequestedBlocks(int piece) { - return pieceIndexToRequestedBlocks.get(piece); + return pieceIndexToRequestedBlocks.computeIfAbsent(piece, k -> new BitSet()); } public synchronized void setBlockRequested(int pieceIndex, int blockIndex) { - BitSet requestedBlocks = pieceIndexToRequestedBlocks.get(pieceIndex); + BitSet requestedBlocks = getRequestedBlocks(pieceIndex); requestedBlocks.set(blockIndex); if (hasUnavailableAndUnrequestedBlocks(pieceIndex)) { @@ -194,7 +264,7 @@ private boolean isPiecePartiallyMissing(int piece) { } public synchronized void setBlockNotRequested(int pieceIndex, int blockIndex) { - BitSet requestedBlocks = pieceIndexToRequestedBlocks.get(pieceIndex); + BitSet requestedBlocks = getRequestedBlocks(pieceIndex); requestedBlocks.clear(blockIndex); if (isPieceCompletelyMissing(pieceIndex) && hasUnavailableAndUnrequestedBlocks(pieceIndex)) { @@ -219,7 +289,7 @@ public synchronized void setBlockReceived(int pieceIndex, int blockIndex) { } private boolean isBlockAvailable(int piece, int blockIndex) { - return pieceIndexToAvailableBlocks.get(piece).get(blockIndex); + return getAvailableBlocks(piece).get(blockIndex); } private void setPieceComplete(int piece) { @@ -294,10 +364,70 @@ public synchronized BitSet getMissingBlocks(int piece) { } private boolean hasUnrequestedBlocks(int piece) { - return pieceIndexToRequestedBlocks.get(piece).cardinality() < fileInfo.getNumBlocks(piece); + return getRequestedBlocks(piece).cardinality() < fileInfo.getNumBlocks(piece); } private boolean hasUnavailableBlocks(int piece) { - return pieceIndexToAvailableBlocks.get(piece).cardinality() < fileInfo.getNumBlocks(piece); + return getAvailableBlocks(piece).cardinality() < fileInfo.getNumBlocks(piece); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TorrentProgress that = (TorrentProgress) o; + return checkedBytes == that.checkedBytes + && fileInfo.equals(that.fileInfo) + && pathToFileProgress.equals(that.pathToFileProgress) + && (verifiedBytes.get() == that.verifiedBytes.get()) + && completePieces.equals(that.completePieces) + && verifiedPieces.equals(that.verifiedPieces) + && pieceIndexToRequestedBlocks.equals(that.pieceIndexToRequestedBlocks) + && pieceIndexToAvailableBlocks.equals(that.pieceIndexToAvailableBlocks) + && partiallyMissingPieces.equals(that.partiallyMissingPieces) + && partiallyMissingPiecesWithUnrequestedBlocks.equals(that.partiallyMissingPiecesWithUnrequestedBlocks) + && completelyMissingPieces.equals(that.completelyMissingPieces) + && completelyMissingPiecesWithUnrequestedBlocks.equals( + that.completelyMissingPiecesWithUnrequestedBlocks); + } + + @Override + public int hashCode() { + int result = fileInfo.hashCode(); + result = 31 * result + pathToFileProgress.hashCode(); + result = 31 * result + verifiedBytes.hashCode(); + result = 31 * result + completePieces.hashCode(); + result = 31 * result + verifiedPieces.hashCode(); + result = 31 * result + pieceIndexToRequestedBlocks.hashCode(); + result = 31 * result + pieceIndexToAvailableBlocks.hashCode(); + result = 31 * result + partiallyMissingPieces.hashCode(); + result = 31 * result + partiallyMissingPiecesWithUnrequestedBlocks.hashCode(); + result = 31 * result + completelyMissingPieces.hashCode(); + result = 31 * result + completelyMissingPiecesWithUnrequestedBlocks.hashCode(); + result = 31 * result + Long.hashCode(checkedBytes); + return result; + } + + @Override + public String toString() { + return "TorrentProgress{" + + "fileInfo=" + fileInfo + + ", pathToFileProgress=" + pathToFileProgress + + ", verifiedBytes=" + verifiedBytes + + ", completePieces=" + completePieces + + ", verifiedPieces=" + verifiedPieces + + ", pieceIndexToRequestedBlocks=" + pieceIndexToRequestedBlocks + + ", pieceIndexToAvailableBlocks=" + pieceIndexToAvailableBlocks + + ", partiallyMissingPieces=" + partiallyMissingPieces + + ", partiallyMissingPiecesWithUnrequestedBlocks=" + partiallyMissingPiecesWithUnrequestedBlocks + + ", completelyMissingPieces=" + completelyMissingPieces + + ", completelyMissingPiecesWithUnrequestedBlocks=" + completelyMissingPiecesWithUnrequestedBlocks + + ", checkedBytes=" + checkedBytes + + '}'; } } diff --git a/src/main/java/jtorrent/domain/torrent/model/TorrentStatistics.java b/src/main/java/jtorrent/domain/torrent/model/TorrentStatistics.java index 55a0aa54..964453ae 100644 --- a/src/main/java/jtorrent/domain/torrent/model/TorrentStatistics.java +++ b/src/main/java/jtorrent/domain/torrent/model/TorrentStatistics.java @@ -1,5 +1,7 @@ package jtorrent.domain.torrent.model; +import static jtorrent.domain.common.util.ValidationUtil.requireNonNegative; + import java.util.concurrent.atomic.AtomicLong; import io.reactivex.rxjava3.core.Observable; @@ -7,11 +9,26 @@ public class TorrentStatistics { - private final AtomicLong downloaded = new AtomicLong(0); - private final BehaviorSubject downloadedSubject = BehaviorSubject.createDefault(0L); + private final AtomicLong downloaded; + private final BehaviorSubject downloadedSubject; + + private final AtomicLong uploaded; + private final BehaviorSubject uploadedSubject; + + public TorrentStatistics(long downloaded, long uploaded) { + requireNonNegative(downloaded); + requireNonNegative(uploaded); + + this.downloaded = new AtomicLong(downloaded); + this.downloadedSubject = BehaviorSubject.createDefault(downloaded); - private final AtomicLong uploaded = new AtomicLong(0); - private final BehaviorSubject uploadedSubject = BehaviorSubject.createDefault(0L); + this.uploaded = new AtomicLong(uploaded); + this.uploadedSubject = BehaviorSubject.createDefault(uploaded); + } + + public static TorrentStatistics createNew() { + return new TorrentStatistics(0, 0); + } public void incrementDownloaded(long bytes) { downloadedSubject.onNext(downloaded.addAndGet(bytes)); @@ -36,4 +53,32 @@ public long getUploaded() { public Observable getUploadedObservable() { return uploadedSubject; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TorrentStatistics that = (TorrentStatistics) o; + return (downloaded.get() == that.downloaded.get()) && (uploaded.get() == that.uploaded.get()); + } + + @Override + public int hashCode() { + int result = downloaded.hashCode(); + result = 31 * result + uploaded.hashCode(); + return result; + } + + @Override + public String toString() { + return "TorrentStatistics{" + + "uploaded=" + uploaded + + ", downloaded=" + downloaded + + '}'; + } } diff --git a/src/main/java/jtorrent/domain/torrent/repository/TorrentMetadataRepository.java b/src/main/java/jtorrent/domain/torrent/repository/TorrentMetadataRepository.java new file mode 100644 index 00000000..da32569c --- /dev/null +++ b/src/main/java/jtorrent/domain/torrent/repository/TorrentMetadataRepository.java @@ -0,0 +1,21 @@ +package jtorrent.domain.torrent.repository; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.List; + +import jtorrent.domain.torrent.model.TorrentMetadata; + +public interface TorrentMetadataRepository { + + TorrentMetadata getTorrentMetadata(File file) throws IOException; + + TorrentMetadata getTorrentMetadata(URL url) throws IOException; + + void saveTorrentMetadata(TorrentMetadata torrentMetadata, Path savePath) throws IOException; + + TorrentMetadata createTOrrentMetadata(Path source, List> trackerUrls, String comment, String createdBy, + int pieceSize) throws IOException; +} diff --git a/src/main/java/jtorrent/domain/torrent/repository/TorrentRepository.java b/src/main/java/jtorrent/domain/torrent/repository/TorrentRepository.java index 41e691f1..35145555 100644 --- a/src/main/java/jtorrent/domain/torrent/repository/TorrentRepository.java +++ b/src/main/java/jtorrent/domain/torrent/repository/TorrentRepository.java @@ -1,32 +1,18 @@ package jtorrent.domain.torrent.repository; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Path; -import java.util.List; - import jtorrent.domain.common.util.Sha1Hash; import jtorrent.domain.common.util.rx.RxObservableList; import jtorrent.domain.torrent.model.Torrent; -import jtorrent.domain.torrent.model.TorrentMetadata; public interface TorrentRepository { - void addTorrent(Torrent torrent); + RxObservableList getTorrents(); - TorrentMetadata loadTorrent(File file) throws IOException; + Torrent getTorrent(Sha1Hash infoHash); - TorrentMetadata loadTorrent(URL url) throws IOException; + void addTorrent(Torrent torrent); - void saveTorrent(TorrentMetadata torrentMetadata, Path savePath) throws IOException; + void persistTorrents(); void removeTorrent(Torrent torrent); - - RxObservableList getTorrents(); - - Torrent getTorrent(Sha1Hash infoHash); - - TorrentMetadata createNewTorrent(Path source, List> trackerUrls, String comment, String createdBy, - int pieceSize) throws IOException; } diff --git a/src/main/java/jtorrent/domain/tracker/model/http/HttpTracker.java b/src/main/java/jtorrent/domain/tracker/model/http/HttpTracker.java index 9b9675f5..f534d48a 100644 --- a/src/main/java/jtorrent/domain/tracker/model/http/HttpTracker.java +++ b/src/main/java/jtorrent/domain/tracker/model/http/HttpTracker.java @@ -88,4 +88,22 @@ public URI getUri() { private String encodeValue(String value) { return URLEncoder.encode(value, StandardCharsets.ISO_8859_1); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HttpTracker that = (HttpTracker) o; + return uri.equals(that.uri); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } } diff --git a/src/main/java/jtorrent/presentation/JTorrent.java b/src/main/java/jtorrent/presentation/JTorrent.java index 25371b3f..6819e692 100644 --- a/src/main/java/jtorrent/presentation/JTorrent.java +++ b/src/main/java/jtorrent/presentation/JTorrent.java @@ -7,14 +7,16 @@ import javafx.application.Application; import javafx.stage.Stage; -import jtorrent.data.torrent.repository.FilePieceRepository; -import jtorrent.data.torrent.repository.FileTorrentRepository; +import jtorrent.data.torrent.repository.AppPieceRepository; +import jtorrent.data.torrent.repository.AppTorrentMetadataRepository; +import jtorrent.data.torrent.repository.AppTorrentRepository; import jtorrent.domain.Client; import jtorrent.domain.common.Constants; import jtorrent.domain.dht.DhtClient; import jtorrent.domain.inbound.InboundConnectionListener; import jtorrent.domain.lsd.LocalServiceDiscoveryManager; import jtorrent.domain.torrent.repository.PieceRepository; +import jtorrent.domain.torrent.repository.TorrentMetadataRepository; import jtorrent.domain.torrent.repository.TorrentRepository; import jtorrent.presentation.main.viewmodel.MainViewModel; @@ -32,10 +34,11 @@ public void init() throws Exception { DhtClient dhtClient = new DhtClient(Constants.PORT); - TorrentRepository repository = new FileTorrentRepository(); - PieceRepository pieceRepository = new FilePieceRepository(); - client = new Client(repository, pieceRepository, inboundConnectionListener, new LocalServiceDiscoveryManager(), - dhtClient); + TorrentRepository torrentRepository = new AppTorrentRepository(); + TorrentMetadataRepository torrentMetadataRepository = new AppTorrentMetadataRepository(); + PieceRepository pieceRepository = new AppPieceRepository(); + client = new Client(torrentRepository, torrentMetadataRepository, pieceRepository, inboundConnectionListener, + new LocalServiceDiscoveryManager(), dhtClient); } @Override diff --git a/src/main/java/jtorrent/presentation/main/viewmodel/MainViewModel.java b/src/main/java/jtorrent/presentation/main/viewmodel/MainViewModel.java index 749901e5..1646aa7d 100644 --- a/src/main/java/jtorrent/presentation/main/viewmodel/MainViewModel.java +++ b/src/main/java/jtorrent/presentation/main/viewmodel/MainViewModel.java @@ -64,12 +64,12 @@ public ChartViewModel getChartViewModel() { } public TorrentMetadata loadTorrent(File file) throws IOException { - return client.loadTorrent(file); + return client.loadTorrentMetadata(file); } public TorrentMetadata loadTorrent(String urlString) throws IOException { URL url = new URL(urlString); - return client.loadTorrent(url); + return client.loadTorrentMetadata(url); } public void addTorrent(TorrentMetadata torrentMetadata, AddNewTorrentDialog.Result result) { diff --git a/src/main/java/jtorrent/presentation/main/viewmodel/TorrentControlsViewModel.java b/src/main/java/jtorrent/presentation/main/viewmodel/TorrentControlsViewModel.java index e195605f..50a88ddc 100644 --- a/src/main/java/jtorrent/presentation/main/viewmodel/TorrentControlsViewModel.java +++ b/src/main/java/jtorrent/presentation/main/viewmodel/TorrentControlsViewModel.java @@ -75,11 +75,11 @@ public void removeSelectedTorrent() { public TorrentMetadata loadTorrentContents(String urlString) throws IOException { URL url = new URL(urlString); - return client.loadTorrent(url); + return client.loadTorrentMetadata(url); } public TorrentMetadata loadTorrentContents(File file) throws IOException { - return client.loadTorrent(file); + return client.loadTorrentMetadata(file); } public void addTorrent(TorrentMetadata torrentMetadata, AddNewTorrentDialog.Result result) { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java deleted file mode 100644 index 7cbe2780..00000000 --- a/src/main/java/module-info.java +++ /dev/null @@ -1,21 +0,0 @@ -module jtorrent { - requires java.desktop; - requires java.logging; - requires javafx.controls; - requires javafx.fxml; - requires io.reactivex.rxjava3; - requires com.dampcake.bencode; - requires atlantafx.base; - requires org.kordamp.ikonli.javafx; - requires org.kordamp.ikonli.materialdesign2; - requires org.slf4j; - - opens jtorrent.presentation to javafx.graphics; - opens jtorrent.presentation.common.component to javafx.fxml; - opens jtorrent.presentation.main.view to javafx.fxml; - opens jtorrent.presentation.addnewtorrent.view to javafx.fxml; - opens jtorrent.presentation.peerinput.view to javafx.fxml; - opens jtorrent.presentation.createnewtorrent.view to javafx.fxml; - opens jtorrent.presentation.exception.view to javafx.fxml; - opens jtorrent.presentation.preferences.view to javafx.fxml; -} diff --git a/src/main/resources/hibernate.properties b/src/main/resources/hibernate.properties new file mode 100644 index 00000000..a94b5cdb --- /dev/null +++ b/src/main/resources/hibernate.properties @@ -0,0 +1,13 @@ +# Database connection settings +hibernate.connection.url=jdbc:h2:file:file:./jtorrent;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE +hibernate.connection.username=sa +hibernate.connection.password= + +# Echo all executed SQL to console +hibernate.show_sql=false +hibernate.format_sql=true +hibernate.highlight_sql=true +hibernate.generate_statistics=false + +# Automatically export the schema +hibernate.hbm2ddl.auto=update \ No newline at end of file diff --git a/src/main/resources/jtorrent/jul.properties b/src/main/resources/jtorrent/jul.properties deleted file mode 100644 index e5dcbd31..00000000 --- a/src/main/resources/jtorrent/jul.properties +++ /dev/null @@ -1,9 +0,0 @@ -handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler -.level=ALL -# ConsoleHandler configuration -java.util.logging.ConsoleHandler.level=ALL -java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter -# FileHandler configuration -java.util.logging.FileHandler.pattern=JTorrent.%u.%g.log -java.util.logging.FileHandler.level=ALL -java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 44355aed..2628652d 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -24,7 +24,7 @@ - + diff --git a/src/test/java/jtorrent/data/torrent/source/db/model/TorrentEntityTest.java b/src/test/java/jtorrent/data/torrent/source/db/model/TorrentEntityTest.java new file mode 100644 index 00000000..c9b3d968 --- /dev/null +++ b/src/test/java/jtorrent/data/torrent/source/db/model/TorrentEntityTest.java @@ -0,0 +1,184 @@ +package jtorrent.data.torrent.source.db.model; + +import static jtorrent.data.torrent.source.db.model.testutil.TestUtil.createBitSetWithRange; +import static org.instancio.Assign.valueOf; +import static org.instancio.Select.all; +import static org.instancio.Select.field; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.nio.file.Path; +import java.util.BitSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import org.instancio.Instancio; +import org.instancio.Model; +import org.junit.jupiter.api.Test; + +import jtorrent.domain.common.util.rx.MutableRxObservableSet; +import jtorrent.domain.torrent.model.FileInfo; +import jtorrent.domain.torrent.model.FileMetadata; +import jtorrent.domain.torrent.model.FileProgress; +import jtorrent.domain.torrent.model.MultiFileInfo; +import jtorrent.domain.torrent.model.SingleFileInfo; +import jtorrent.domain.torrent.model.Torrent; +import jtorrent.domain.torrent.model.TorrentMetadata; +import jtorrent.domain.torrent.model.TorrentProgress; +import jtorrent.domain.tracker.model.factory.TrackerFactory; + +class TorrentEntityTest { + + private static final Model TORRENT_PROGRESS_MODEL = Instancio.of(TorrentProgress.class) + .set(field("verifiedPieces"), new BitSet()) + .set(field("completePieces"), new BitSet()) + .set(field("pieceIndexToRequestedBlocks"), Map.of()) + .set(field("pieceIndexToAvailableBlocks"), Map.of()) + .set(field("partiallyMissingPieces"), new BitSet()) + .set(field("partiallyMissingPiecesWithUnrequestedBlocks"), new BitSet()) + .set(field("completelyMissingPieces"), new BitSet()) + .set(field("completelyMissingPiecesWithUnrequestedBlocks"), new BitSet()) + .set(field("verifiedBytes"), new AtomicLong(0)) + .set(field("checkedBytes"), 0) + .toModel(); + + private static final Model FILE_PROGRESS_MODEL = Instancio.of(FileProgress.class) + .set(field("verifiedPieces"), new BitSet()) + .set(field("verifiedBytes"), new AtomicLong(0)) + .toModel(); + + @Test + void bidirectionalMapping_singleFile() { + FileMetadata fileMetadata = Instancio.of(FileMetadata.class) + .set(field("size"), 100) + .set(field("path"), Path.of("file.txt")) + .set(field("firstPiece"), 0) + .set(field("firstPieceStart"), 0) + .set(field("lastPiece"), 9) + .set(field("lastPieceEnd"), 9) + .set(field("start"), 0) + .create(); + + SingleFileInfo fileInfo = Instancio.of(SingleFileInfo.class) + .generate(all(byte[].class), gen -> gen.array().length(20)) + .generate(field(FileInfo.class, "pieceHashes"), gen -> gen.collection().size(10)) + .set(field(FileInfo.class, "pieceSize"), 10) + .set(field(FileInfo.class, "fileMetaData"), List.of(fileMetadata)) + .create(); + + Torrent expected = Instancio.of(Torrent.class) + .assign(valueOf(field(TorrentMetadata.class, "trackerTiers")) + .to(field(Torrent.class, "trackers")) + .as(trackerTiers -> ((List>) trackerTiers) + .get(0) + .stream() + .map(TrackerFactory::fromUri) + .collect(Collectors.toSet()) + ) + ) + .set(field("peers"), new MutableRxObservableSet<>(Set.of())) + .set(field(TorrentMetadata.class, "fileInfo"), fileInfo) + .set(field("torrentProgress"), + Instancio.of(TORRENT_PROGRESS_MODEL) + .set(field("fileInfo"), fileInfo) + .set(field("completelyMissingPieces"), createBitSetWithRange(0, 10)) + .set(field("completelyMissingPiecesWithUnrequestedBlocks"), + createBitSetWithRange(0, 10)) + .set(field("pathToFileProgress"), + Map.of( + Path.of("file.txt"), + Instancio.of(FILE_PROGRESS_MODEL) + .set(field("fileInfo"), fileInfo) + .set(field("fileMetaData"), fileMetadata) + .create() + ) + ) + .create() + ) + .create(); + + TorrentEntity torrentEntity = TorrentEntity.fromDomain(expected); + Torrent actual = torrentEntity.toDomain(); + + assertEquals(expected, actual); + } + + @Test + void bidirectionalMapping_multiFile() { + FileMetadata fileMetadata1 = Instancio.of(FileMetadata.class) + .set(field("size"), 100) + .set(field("path"), Path.of("file1.txt")) + .set(field("firstPiece"), 0) + .set(field("firstPieceStart"), 0) + .set(field("lastPiece"), 9) + .set(field("lastPieceEnd"), 9) + .set(field("start"), 0) + .create(); + + FileMetadata fileMetadata2 = Instancio.of(FileMetadata.class) + .set(field("size"), 100) + .set(field("path"), Path.of("file2.txt")) + .set(field("firstPiece"), 10) + .set(field("firstPieceStart"), 0) + .set(field("lastPiece"), 19) + .set(field("lastPieceEnd"), 9) + .set(field("start"), 100) + .create(); + + MultiFileInfo fileInfo = Instancio.of(MultiFileInfo.class) + .generate(all(byte[].class), gen -> gen.array().length(20)) + .generate(field(FileInfo.class, "pieceHashes"), gen -> gen.collection().size(20)) + .set(field(FileInfo.class, "pieceSize"), 10) + .set(field(FileInfo.class, "fileMetaData"), + List.of( + fileMetadata1, + fileMetadata2 + ) + ) + .create(); + + Torrent expected = Instancio.of(Torrent.class) + .assign(valueOf(field(TorrentMetadata.class, "trackerTiers")) + .to(field(Torrent.class, "trackers")) + .as(trackerTiers -> ((List>) trackerTiers) + .get(0) + .stream() + .map(TrackerFactory::fromUri) + .collect(Collectors.toSet()) + ) + ) + .set(field("peers"), new MutableRxObservableSet<>(Set.of())) + .set(field(TorrentMetadata.class, "fileInfo"), fileInfo) + .set(field("torrentProgress"), + Instancio.of(TORRENT_PROGRESS_MODEL) + .set(field("fileInfo"), fileInfo) + .set(field("completelyMissingPieces"), createBitSetWithRange(0, 20)) + .set(field("completelyMissingPiecesWithUnrequestedBlocks"), + createBitSetWithRange(0, 20)) + .set(field("pathToFileProgress"), + Map.of( + Path.of("file1.txt"), + Instancio.of(FILE_PROGRESS_MODEL) + .set(field("fileInfo"), fileInfo) + .set(field("fileMetaData"), fileMetadata1) + .create(), + Path.of("file2.txt"), + Instancio.of(FILE_PROGRESS_MODEL) + .set(field("fileInfo"), fileInfo) + .set(field("fileMetaData"), fileMetadata2) + .create() + ) + ) + .create() + ) + .create(); + + TorrentEntity torrentEntity = TorrentEntity.fromDomain(expected); + Torrent actual = torrentEntity.toDomain(); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/jtorrent/data/torrent/source/db/model/TorrentProgressComponentTest.java b/src/test/java/jtorrent/data/torrent/source/db/model/TorrentProgressComponentTest.java new file mode 100644 index 00000000..e81f6a56 --- /dev/null +++ b/src/test/java/jtorrent/data/torrent/source/db/model/TorrentProgressComponentTest.java @@ -0,0 +1,81 @@ +package jtorrent.data.torrent.source.db.model; + +import static jtorrent.data.torrent.source.db.model.testutil.TestUtil.createBitSet; +import static org.instancio.Select.all; +import static org.instancio.Select.field; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; +import java.util.BitSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import org.instancio.Instancio; +import org.junit.jupiter.api.Test; + +import jtorrent.domain.torrent.model.FileInfo; +import jtorrent.domain.torrent.model.FileMetadata; +import jtorrent.domain.torrent.model.FileProgress; +import jtorrent.domain.torrent.model.SingleFileInfo; +import jtorrent.domain.torrent.model.TorrentProgress; + +class TorrentProgressComponentTest { + + @Test + void bidirectionalMapping() { + FileMetadata fileMetadata = Instancio.of(FileMetadata.class) + .set(field("size"), 49) + .set(field("path"), Path.of("file.txt")) + .set(field("firstPiece"), 0) + .set(field("firstPieceStart"), 0) + .set(field("lastPiece"), 4) + .set(field("lastPieceEnd"), 8) + .set(field("start"), 0) + .create(); + + SingleFileInfo fileInfo = Instancio.of(SingleFileInfo.class) + .generate(all(byte[].class), gen -> gen.array().length(20)) + .set(field(FileInfo.class, "pieceSize"), 10) + .generate(field(FileInfo.class, "pieceHashes"), gen -> gen.collection().size(5)) + .set(field(FileInfo.class, "fileMetaData"), List.of(fileMetadata)) + .create(); + + TorrentProgress expected = Instancio.of(TorrentProgress.class) + .set(field("fileInfo"), fileInfo) + .set(field("pathToFileProgress"), + Map.of( + Path.of("file.txt"), + Instancio.of(FileProgress.class) + .set(field("verifiedPieces"), createBitSet(0, 1, 2, 4)) + .set(field("verifiedBytes"), new AtomicLong(39)) + .set(field("fileInfo"), fileInfo) + .set(field("fileMetaData"), fileMetadata) + .create() + ) + ) + .set(field("verifiedPieces"), createBitSet(0, 1, 2, 4)) + .set(field("completePieces"), createBitSet(0, 1, 2, 4)) + .set(field("pieceIndexToRequestedBlocks"), Map.of()) + .set(field("pieceIndexToAvailableBlocks"), + Map.of( + 0, createBitSet(0), + 1, createBitSet(0), + 2, createBitSet(0), + 4, createBitSet(0) + ) + ) + .set(field("partiallyMissingPieces"), new BitSet()) + .set(field("partiallyMissingPiecesWithUnrequestedBlocks"), new BitSet()) + .set(field("completelyMissingPieces"), createBitSet(3)) + .set(field("completelyMissingPiecesWithUnrequestedBlocks"), createBitSet(3)) + .set(field("verifiedBytes"), new AtomicLong(39)) + .set(field("checkedBytes"), 0) + .create(); + + TorrentProgressComponent torrentProgressComponent = TorrentProgressComponent.fromDomain(expected); + TorrentProgress actual = torrentProgressComponent.toDomain(fileInfo); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/jtorrent/data/torrent/source/db/model/testutil/TestUtil.java b/src/test/java/jtorrent/data/torrent/source/db/model/testutil/TestUtil.java new file mode 100644 index 00000000..cf0625fc --- /dev/null +++ b/src/test/java/jtorrent/data/torrent/source/db/model/testutil/TestUtil.java @@ -0,0 +1,23 @@ +package jtorrent.data.torrent.source.db.model.testutil; + +import java.util.BitSet; + +public class TestUtil { + + private TestUtil() { + } + + public static BitSet createBitSet(int... bits) { + BitSet bitSet = new BitSet(); + for (int bit : bits) { + bitSet.set(bit); + } + return bitSet; + } + + public static BitSet createBitSetWithRange(int from, int to) { + BitSet bitSet = new BitSet(); + bitSet.set(from, to); + return bitSet; + } +} diff --git a/src/test/java/jtorrent/data/torrent/model/BencodedTorrentTest.java b/src/test/java/jtorrent/data/torrent/source/file/model/BencodedTorrentTest.java similarity index 96% rename from src/test/java/jtorrent/data/torrent/model/BencodedTorrentTest.java rename to src/test/java/jtorrent/data/torrent/source/file/model/BencodedTorrentTest.java index d2e276e5..a5b41b2a 100644 --- a/src/test/java/jtorrent/data/torrent/model/BencodedTorrentTest.java +++ b/src/test/java/jtorrent/data/torrent/source/file/model/BencodedTorrentTest.java @@ -1,4 +1,4 @@ -package jtorrent.data.torrent.model; +package jtorrent.data.torrent.source.file.model; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -16,7 +16,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import org.junit.jupiter.api.Test; @@ -239,7 +238,7 @@ void toDomain_singleFile() throws NoSuchAlgorithmException, IOException { TorrentMetadata actual = bencodedTorrent.toDomain(); TorrentMetadata expected = new TorrentMetadataBuilder() - .setTrackers(Set.of(URI.create("udp://tracker.example.com:80/announce"))) + .setTrackers(List.of(List.of(URI.create("udp://tracker.example.com:80/announce")))) .setCreationDate(LocalDateTime.ofEpochSecond(123456789L, 0, OffsetDateTime.now().getOffset())) .setComment("comment") .setCreatedBy("created by") @@ -255,7 +254,6 @@ void toDomain_singleFile() throws NoSuchAlgorithmException, IOException { .setLastPiece(0) .setLastPieceEnd(99) .setStart(0) - .setEnd(99) .build() )) .setInfoHash(new Sha1Hash(info.getInfoHash())) @@ -292,7 +290,7 @@ void toDomain_multiFile() throws NoSuchAlgorithmException, IOException { TorrentMetadata actual = bencodedTorrent.toDomain(); TorrentMetadata expected = new TorrentMetadataBuilder() - .setTrackers(Set.of(URI.create("udp://tracker.example.com:80/announce"))) + .setTrackers(List.of(List.of(URI.create("udp://tracker.example.com:80/announce")))) .setCreationDate(LocalDateTime.ofEpochSecond(123456789L, 0, OffsetDateTime.now().getOffset())) .setComment("comment") .setCreatedBy("created by") @@ -309,7 +307,6 @@ void toDomain_multiFile() throws NoSuchAlgorithmException, IOException { .setLastPiece(0) .setLastPieceEnd(99) .setStart(0) - .setEnd(99) .build(), new FileMetadataBuilder() .setLength(200) @@ -319,7 +316,6 @@ void toDomain_multiFile() throws NoSuchAlgorithmException, IOException { .setLastPiece(2) .setLastPieceEnd(99) .setStart(100) - .setEnd(299) .build() )) .setInfoHash(new Sha1Hash(info.getInfoHash())) @@ -458,7 +454,7 @@ public BencodedFile build() { private static class TorrentMetadataBuilder { - private Set trackers = Collections.emptySet(); + private List> trackers = Collections.emptyList(); private LocalDateTime creationDate = LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC); private String comment = ""; private String createdBy = ""; @@ -469,7 +465,7 @@ private static class TorrentMetadataBuilder { private List fileMetadata = Collections.emptyList(); private Sha1Hash infoHash = new Sha1Hash(new byte[20]); - public TorrentMetadataBuilder setTrackers(Set trackers) { + public TorrentMetadataBuilder setTrackers(List> trackers) { this.trackers = trackers; return this; } @@ -543,7 +539,6 @@ private static class FileMetadataBuilder { private int lastPiece; private int lastPieceEnd; private long start; - private long end; public FileMetadataBuilder setLength(int length) { this.length = length; @@ -580,13 +575,8 @@ public FileMetadataBuilder setStart(long start) { return this; } - public FileMetadataBuilder setEnd(long end) { - this.end = end; - return this; - } - public FileMetadata build() { - return new FileMetadata(length, path, firstPiece, firstPieceStart, lastPiece, lastPieceEnd, start, end); + return new FileMetadata(path, start, length, firstPiece, firstPieceStart, lastPiece, lastPieceEnd); } } } diff --git a/src/test/resources/instancio.properties b/src/test/resources/instancio.properties new file mode 100644 index 00000000..ff2429e6 --- /dev/null +++ b/src/test/resources/instancio.properties @@ -0,0 +1 @@ +seed = 1