diff --git a/.gitignore b/.gitignore index f2e1fa5..3dbdec8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,602 +1,29 @@ +# Compiled class file +*.class -# Created by https://www.gitignore.io/api/c++,latex,cmake,netbeans,qtcreator,visualstudio - -### C++ ### -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -### CMake ### -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -build - -### LaTeX ### -## Core latex/pdflatex auxiliary files: -*.aux -*.lof +# Log file *.log -*.lot -*.fls -*.toc -*.fmt -*.fot -*.cb -*.cb2 - -## Intermediate documents: -*.dvi -*.xdv -*-converted-to.* -# these rules might exclude image files for figures etc. -# *.ps -# *.eps -# *.pdf - -## Generated if empty string is given at "Please type another file name for output:" -.pdf - -## Bibliography auxiliary files (bibtex/biblatex/biber): -*.bbl -*.bcf -*.blg -*-blx.aux -*-blx.bib -*.run.xml - -## Build tool auxiliary files: -*.fdb_latexmk -*.synctex -*.synctex(busy) -*.synctex.gz -*.synctex.gz(busy) -*.pdfsync -*Notes.bib - -## Auxiliary and intermediate files from other packages: -# algorithms -*.alg -*.loa - -# achemso -acs-*.bib - -# amsthm -*.thm - -# beamer -*.nav -*.pre -*.snm -*.vrb - -# changes -*.soc - -# cprotect -*.cpt - -# elsarticle (documentclass of Elsevier journals) -*.spl - -# endnotes -*.ent - -# fixme -*.lox - -# feynmf/feynmp -*.mf -*.mp -*.t[1-9] -*.t[1-9][0-9] -*.tfm - -#(r)(e)ledmac/(r)(e)ledpar -*.end -*.?end -*.[1-9] -*.[1-9][0-9] -*.[1-9][0-9][0-9] -*.[1-9]R -*.[1-9][0-9]R -*.[1-9][0-9][0-9]R -*.eledsec[1-9] -*.eledsec[1-9]R -*.eledsec[1-9][0-9] -*.eledsec[1-9][0-9]R -*.eledsec[1-9][0-9][0-9] -*.eledsec[1-9][0-9][0-9]R - -# glossaries -*.acn -*.acr -*.glg -*.glo -*.gls -*.glsdefs - -# gnuplottex -*-gnuplottex-* - -# gregoriotex -*.gaux -*.gtex - -# hyperref -*.brf - -# knitr -*-concordance.tex -# TODO Comment the next line if you want to keep your tikz graphics files -*.tikz -*-tikzDictionary - -# listings -*.lol - -# makeidx -*.idx -*.ilg -*.ind -*.ist - -# minitoc -*.maf -*.mlf -*.mlt -*.mtc[0-9]* -*.slf[0-9]* -*.slt[0-9]* -*.stc[0-9]* - -# minted -_minted* -*.pyg - -# morewrites -*.mw - -# nomencl -*.nlo - -# pax -*.pax - -# pdfpcnotes -*.pdfpc - -# sagetex -*.sagetex.sage -*.sagetex.py -*.sagetex.scmd - -# scrwfile -*.wrt - -# sympy -*.sout -*.sympy -sympy-plots-for-*.tex/ - -# pdfcomment -*.upa -*.upb - -# pythontex -*.pytxcode -pythontex-files-*/ - -# thmtools -*.loe - -# TikZ & PGF -*.dpth -*.md5 -*.auxlock - -# todonotes -*.tdo - -# easy-todo -*.lod - -# xindy -*.xdy - -# xypic precompiled matrices -*.xyc - -# endfloat -*.ttt -*.fff - -# Latexian -TSWLatexianTemp* - -## Editors: -# WinEdt -*.bak -*.sav - -# Texpad -.texpadtmp - -# Kile -*.backup - -# KBibTeX -*~[0-9]* - -# auto folder when using emacs and auctex -/auto/* - -# expex forward references with \gathertags -*-tags.tex - -### NetBeans ### -nbproject/private/ -build/ -nbbuild/ -dist/ -nbdist/ -.nb-gradle/ - -### QtCreator ### -# gitignore for Qt Creator like IDE for pure C/C++ project without Qt -# -# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html - - -# Qt Creator autogenerated files +# BlueJ files +*.ctxt +# Mobile Tools for Java (J2ME) +.mtj.tmp/ -# A listing of all the files included in the project -*.files +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar -# Include directories -*.includes +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* -# Project configuration settings like predefined Macros -*.config +# Gradle wrapper +!gradle/wrapper/gradle-wrapper.jar -# Qt Creator settings -*.creator - -# User project settings -*.creator.user* - -# Qt Creator backups -*.autosave - -### VisualStudio ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Uncomment the next line to ignore your web deploy settings. -# By default, sensitive information, such as encrypted password -# should be stored in the .pubxml.user file. -#*.pubxml -*.pubxml.user -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider +.gradle/ .idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -### VisualStudio Patch ### -# By default, sensitive information, such as encrypted password -# should be stored in the .pubxml.user file. - -# End of https://www.gitignore.io/api/c++,latex,cmake,netbeans,qtcreator,visualstudio +build/ diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..37d9c8f --- /dev/null +++ b/Readme.md @@ -0,0 +1,150 @@ +# Torrent + +* На трекере хранится список файлов и информация об активных пользователях, у которых есть те или иные файлы (возможно не целиком). + +* С помощью клиентского приложения можно просматривать список файлов на трекере, а также добавлять новые и выбирать файлы из списка для скачивания. + +* Файлы условно разбиваются на последовательные блоки бинарных данных константного размера (например 10M). Последний блок может иметь меньший размер. Блоки нумеруются с нуля. + +--- + +# Torrent + +* Клиент при подключении отправляет на трекер список раздаваемых им файлов. + +* При скачивании файла клиент получает у трекера информацию о клиентах, раздающих файл (сидах), и далее общается с ними напрямую. + +* У отдельного сида можно узнать о том, какие полные части у него есть, а также скачать их. + +* После скачивания отдельных блоков некоторого файла клиент становится сидом. + +--- + +# Torrent-tracker + +* Хранит мета-информацию о раздаваемых файлах: + * идентификатор + * активные клиенты (недавно был update), у которых есть этот файл целиком или некоторые его части + +* Порт сервера: 8081 + +* Запросы: + * list — список раздаваемых файлов + * upload — публикация нового файла + * sources — список клиентов, владеющих определенным файлов целиком или некоторыми его частями + * update — загрузка клиентом данных о раздаваемых файлах + +--- + +# List + +Формат запроса: + + <1: Byte> +Формат ответа: + + ( )*, + count — количество файлов + id — идентификатор файла + name — название файла + size — размер файла + +--- + +# Upload + +Формат запроса: + + <2: Byte> , + name — название файла + size — размер файла +Формат ответа: + + , + id — идентификатор файла + +# Примечание + +* Если клиент А и клиент Б решили опубликовать файл abc.txt, то это будут **разные** файлы, иными словами каждый запрос на публикацию файла возвращает **новый** id + +--- + +# Sources + +Формат запроса: + + <3: Byte> , + id — идентификатор файла +Формат ответа: + + ( )*, + size — количество клиентов, раздающих файл + ip — ip клиента, + clientPort — порт клиента + +--- + +# Update + +Формат запроса: + + <4: Byte> ()*, + clientPort — порт клиента, + count — количество раздаваемых файлов, + id — идентификатор файла +Формат ответа: + + , + status — True, если информация успешно обновлена + +# Примечание + +* Клиент обязан исполнять данный запрос каждые 5 минут, иначе сервер считает, что клиент ушел с раздачи + +--- + +# Torrent-client + +* Порт клиента указывается при запуске и передается на трекер в рамках запроса update + +* Каждый файл раздается по частям, размер части — константа на всё приложение + +* Клиент хранит и раздает эти самые части + +* Запросы: + * stat — доступные для раздачи части определенного файла + * get — скачивание части определенного файла + +--- + +# Stat + +Формат запроса: + + <1: Byte> , + id — идентификатор файла +Формат ответа: + + ()*, + count — количество доступных частей + part — номер части + +# Примечание + +* Часть считается доступной для раздачи, если она хранится на клиенте целиком + +--- + +# Get + +Формат запроса: + + <2: Byte> + id — идентификатор файла, + part — номер части +Формат ответа: + + , + content — содержимое части + +--- diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..04a4021 --- /dev/null +++ b/build.gradle @@ -0,0 +1,20 @@ +group 'org.itmo' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile project(':tracker') + compile project(':client') + testCompile group: 'junit', name: 'junit', version: '4.12' +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.3' +} diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 0000000..9663dad --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,28 @@ +group 'org.itmo' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jar { + from { + configurations.compile.collect { + it.isDirectory() ? it : zipTree(it) + } + } + manifest { + attributes 'Main-Class': 'org.itmo.torrent.client.TorrentClientMain' + } +} + +dependencies { + compile project(':commons') + compile group: 'commons-io', name: 'commons-io', version: '2.6' + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/client/src/main/java/org/itmo/torrent/client/Client.java b/client/src/main/java/org/itmo/torrent/client/Client.java new file mode 100644 index 0000000..ddab207 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/client/Client.java @@ -0,0 +1,35 @@ +package org.itmo.torrent.client; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.Address; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Set; + +public interface Client { + + @NotNull + List executeList() throws IOException; + + int executeUpload(@NotNull final File file) throws IOException; + + @NotNull + Set
executeSources(final int fileId) throws IOException; + + boolean executeUpdate() throws IOException; + + void executeDownload(final int fileId) throws IOException; + + @NotNull + Set executeStat(@NotNull final Address address, + final int fileId) throws IOException; + + @Nullable + InputStream executeGet(@NotNull final Address address, + final int fileId, final int fileChunkId) throws IOException; +} diff --git a/client/src/main/java/org/itmo/torrent/client/Downloader.java b/client/src/main/java/org/itmo/torrent/client/Downloader.java new file mode 100644 index 0000000..5fc2b23 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/client/Downloader.java @@ -0,0 +1,159 @@ +package org.itmo.torrent.client; + +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.filesystem.FileSeedInfo; +import org.itmo.torrent.network.Address; + +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RecursiveTask; + +public class Downloader implements AutoCloseable { + @NotNull + private final TorrentClient client; + @NotNull + private final ExecutorService executor = Executors.newWorkStealingPool(); + + public Downloader(@NotNull final TorrentClient client) { + this.client = client; + } + + void download(final int fileId) { + executor.submit(new DownloadFileTask(fileId)); + } + + @Override + public void close() { + executor.shutdown(); + } + + private class DownloadFileTask implements Runnable { + private final int fileId; + + DownloadFileTask(final int fileId) { + this.fileId = fileId; + } + + @Override + public void run() { + final FileSeedInfo fileInfo = getFileSeedInfo(); + if (fileInfo == null) { + return; + } + + final Map> fileChunkSeeders = getFileChunksSeeders(); + if (fileChunkSeeders == null) { + return; + } + + try (RandomAccessFile out = new RandomAccessFile(fileInfo.getFile(), "rw")) { + out.setLength(fileInfo.getSize()); + client.getState().addFileInfo(fileInfo); + final List subTasks = fork(fileInfo, fileChunkSeeders); + join(fileInfo, out, subTasks); + } catch (IOException ignore) { + // TODO: proper handling + } + } + + @Nullable + private Map> getFileChunksSeeders() { + // TODO: fix bottleneck + final Map> fileChunkSeeders = new HashMap<>(); + try { + for (Address source : client.executeSources(fileId)) { + for (int chunksId : client.executeStat(source, fileId)) { + fileChunkSeeders.putIfAbsent(chunksId, new HashSet<>()); + fileChunkSeeders.get(chunksId).add(source); + } + } + } catch (IOException e) { + System.err.println(e.getMessage()); + return null; + } + + return fileChunkSeeders; + } + + @NotNull + private List fork(@NotNull final FileSeedInfo fileInfo, + @NotNull final Map> fileChunkSeeders) { + return new ArrayList() {{ + for (int fileChunkId = 0; fileChunkId < fileInfo.getChunksNumber(); fileChunkId++) { + if (!fileInfo.isChunkAvailable(fileChunkId)) { + final DownloadFileChunkTask task = new DownloadFileChunkTask(fileId, fileChunkId, + fileChunkSeeders.getOrDefault(fileChunkId, Collections.emptySet())); + task.fork(); + add(task); + } + } + }}; + } + + private void join(@NotNull final FileSeedInfo fileInfo, @NotNull final RandomAccessFile out, + @NotNull final List subTasks) throws IOException { + for (DownloadFileChunkTask task : subTasks) { + final InputStream fileChunkContent = task.join(); + out.seek(task.fileChunkId * FileInfo.FILE_CHUNK_SIZE); + out.write(IOUtils.toByteArray(fileChunkContent)); + fileInfo.setChunkAvailable(task.fileChunkId); + } + } + + @Nullable + private FileSeedInfo getFileSeedInfo() { + FileSeedInfo fileInfo = client.getState().getFileInfo(fileId); + if (fileInfo == null) { + try { + FileInfo tempFileInfo = client.executeList().stream() + .filter(info -> info.getId() == fileId) + .findAny() + .orElse(null); + fileInfo = tempFileInfo != null ? new FileSeedInfo(tempFileInfo) : null; + } catch (IOException ignored) { + } + } + + return fileInfo; + } + } + + private class DownloadFileChunkTask extends RecursiveTask { + private final int fileId; + private final int fileChunkId; + @NotNull + private final Set
seeders; + + DownloadFileChunkTask(final int fileId, final int fileChunkId, + @NotNull final Set
seeders) { + this.fileId = fileId; + this.fileChunkId = fileChunkId; + this.seeders = seeders; + } + + @Nullable + @Override + protected InputStream compute() { + for (Address address : seeders) { + InputStream fileChunkContent = null; + try { + fileChunkContent = client.executeGet(address, fileId, fileChunkId); + } catch (IOException ignored) { + } + if (fileChunkContent != null) { + return fileChunkContent; + } + } + + return null; + } + } +} + diff --git a/client/src/main/java/org/itmo/torrent/client/Seeder.java b/client/src/main/java/org/itmo/torrent/client/Seeder.java new file mode 100644 index 0000000..21d3512 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/client/Seeder.java @@ -0,0 +1,94 @@ +package org.itmo.torrent.client; + +import org.itmo.torrent.AbstractServer; +import org.itmo.torrent.network.Peer2ClientConnection; +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.filesystem.FileSeedInfo; +import org.itmo.torrent.network.messages.client.Get; +import org.itmo.torrent.network.messages.client.Stat; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.Socket; +import java.util.Collections; +import java.util.Set; + +public class Seeder extends AbstractServer { + @NotNull + private final TorrentClientState state; + + public Seeder(@NotNull final TorrentClientState state, final short port) throws IOException { + super(port); + this.state = state; + } + + @NotNull + @Override + protected Runnable getConnectionHandler(@NotNull final Socket clientSocket) { + return new ConnectionHandler(clientSocket); + } + + private class ConnectionHandler implements Runnable { + @NotNull + final Socket socket; + + public ConnectionHandler(@NotNull final Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + try (Peer2ClientConnection connection = new Peer2ClientConnection(socket)) { + int requestType = connection.receiveRequestType(); + switch (requestType) { + case Stat.ID: + handleStatRequest(connection); + break; + case Get.ID: + handleGetRequest(connection); + break; + default: + throw new IOException("Unknown request type"); + } + } catch (Exception e) { + System.err.println(e.getMessage()); + } + } + + private void handleStatRequest(@NotNull final Peer2ClientConnection connection) + throws IOException { + final Stat.Request request = connection.receiveStatRequest(); + final FileSeedInfo fileInfo = state.getFileInfo(request.getFileId()); + final Set availableChunks = fileInfo != null + ? fileInfo.getSetAvailableChunks() + : Collections.emptySet(); + final Stat.Response response = new Stat.Response(availableChunks); + connection.sendStatResponse(response); + } + + private void handleGetRequest(@NotNull final Peer2ClientConnection connection) + throws IOException { + final Get.Request request = connection.receiveGetRequest(); + final FileSeedInfo fileInfo = state.getFileInfo(request.getFileId()); + + if (fileInfo == null || !fileInfo.isChunkAvailable(request.getFileChunkId())) { + final Get.Response response = new Get.Response(null, -1); + connection.sendGetResponse(response); + return; + } + + final byte[] fileChunkContent; + try (RandomAccessFile file = new RandomAccessFile(fileInfo.getFile(), "rw")) { + file.seek(FileInfo.FILE_CHUNK_SIZE * request.getFileChunkId()); + fileChunkContent = new byte[(int) fileInfo.getChunkSize(request.getFileChunkId())]; + file.readFully(fileChunkContent); + } + + final Get.Response response = new Get.Response( + new ByteArrayInputStream(fileChunkContent), fileChunkContent.length); + connection.sendGetResponse(response); + } + } +} diff --git a/client/src/main/java/org/itmo/torrent/client/TorrentClient.java b/client/src/main/java/org/itmo/torrent/client/TorrentClient.java new file mode 100644 index 0000000..4766e9b --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/client/TorrentClient.java @@ -0,0 +1,158 @@ +package org.itmo.torrent.client; + +import org.itmo.torrent.Constants; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.filesystem.FileSeedInfo; +import org.itmo.torrent.filesystem.FileSystemManager; +import org.itmo.torrent.network.Address; +import org.itmo.torrent.network.Client2PeerConnection; +import org.itmo.torrent.network.Client2TrackerConnection; +import org.itmo.torrent.network.messages.client.Get; +import org.itmo.torrent.network.messages.client.Stat; +import org.itmo.torrent.network.messages.tracker.List; +import org.itmo.torrent.network.messages.tracker.Sources; +import org.itmo.torrent.network.messages.tracker.Update; +import org.itmo.torrent.network.messages.tracker.Upload; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.nio.file.Path; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TorrentClient implements Client, AutoCloseable { + @NotNull + private final TorrentClientState state = new TorrentClientState(); + @NotNull + private final Seeder seeder; + @NotNull + private final ScheduledExecutorService updater = Executors.newScheduledThreadPool(1); + @NotNull + private final Downloader downloader = new Downloader(this); + + public TorrentClient(final short port, @NotNull final Path rootFolderPath) + throws IOException, ClassNotFoundException { + FileSystemManager.getInstance().setRootFolderPath(rootFolderPath); + state.restore(rootFolderPath); + seeder = new Seeder(state, port); + final Runnable updateAction = () -> { + try { + executeUpdate(); + } catch (IOException ignored) { + } + }; + updater.scheduleAtFixedRate(updateAction, 0, Update.UPDATE_PERIOD, TimeUnit.MILLISECONDS); + } + + @Override + public void close() throws IOException { + downloader.close(); + seeder.close(); + updater.shutdown(); + final FileSystemManager manager = FileSystemManager.getInstance(); + state.save(manager.getRootFolderPath()); + } + + @NotNull + @Override + public java.util.List executeList() throws IOException { + try (Client2TrackerConnection connection = connectToTracker()) { + final List.Request request = new List.Request(); + connection.sendListRequest(request); + final List.Response response = connection.receiveListResponse(); + return response.getFileInfos(); + } + } + + @Override + public int executeUpload(@NotNull final File file) throws IOException { + if (!file.isFile()) { + throw new IllegalArgumentException("File doesn't exist, or it's not a normal file"); + } + + try (Client2TrackerConnection connection = connectToTracker()) { + final String fileName = file.getName(); + final long fileSize = file.length(); + final Upload.Request request = new Upload.Request(fileName, fileSize); + connection.sendUploadRequest(request); + final Upload.Response response = connection.receiveUploadResponse(); + final int fileId = response.getFileId(); + final FileSeedInfo fileInfo = new FileSeedInfo(fileId, fileName, fileSize, file); + state.addFileInfo(fileInfo); + return fileId; + } + } + + @NotNull + @Override + public Set
executeSources(final int fileId) throws IOException { + try (Client2TrackerConnection connection = connectToTracker()) { + final Sources.Request request = new Sources.Request(fileId); + connection.sendSourcesRequest(request); + final Sources.Response response = connection.receiveSourcesResponse(); + return response.getClientsAddresses(); + } + } + + @Override + public boolean executeUpdate() throws IOException { + try (Client2TrackerConnection connection = connectToTracker()) { + final Update.Request request = new Update.Request(seeder.getPort(), state.getFilesIds()); + connection.sendUpdateRequest(request); + final Update.Response response = connection.receiveUpdateResponse(); + return response.getStatus(); + } + } + + @Override + public void executeDownload(final int fileId) { + downloader.download(fileId); + } + + @NotNull + @Override + public Set executeStat(@NotNull final Address address, + final int fileId) throws IOException { + try (Client2PeerConnection connection = connectToPeer(address)) { + final Stat.Request request = new Stat.Request(fileId); + connection.sendStatRequest(request); + final Stat.Response response = connection.receiveStatResponse(); + return response.getFileChunksIds(); + } + } + + @Nullable + @Override + public InputStream executeGet(@NotNull final Address address, + final int fileId, final int fileChunkId) throws IOException { + try (Client2PeerConnection connection = connectToPeer(address)) { + final Get.Request request = new Get.Request(fileId, fileChunkId); + connection.sendGetRequest(request); + final Get.Response response = connection.receiveGetResponse(); + return response.getCount() == -1 ? null : response.getFileChunkContent(); + } + } + + @NotNull + TorrentClientState getState() { + return state; + } + + @NotNull + private Client2TrackerConnection connectToTracker() throws IOException { + final Socket socket = new Socket(Constants.TRACKER_HOST, Constants.TRACKER_PORT); + return new Client2TrackerConnection(socket); + } + + @NotNull + private Client2PeerConnection connectToPeer(@NotNull final Address address) throws IOException { + final Socket socket = new Socket(address.getHost(), address.getPort()); + return new Client2PeerConnection(socket); + } +} diff --git a/client/src/main/java/org/itmo/torrent/client/TorrentClientMain.java b/client/src/main/java/org/itmo/torrent/client/TorrentClientMain.java new file mode 100644 index 0000000..bac20f5 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/client/TorrentClientMain.java @@ -0,0 +1,105 @@ +package org.itmo.torrent.client; + +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.Address; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.Scanner; +import java.util.Set; + +public class TorrentClientMain { + @NotNull + private static final String USAGE = "client "; + + @NotNull + private static final String LIST_CMD = "list"; + @NotNull + private static final String UPLOAD_CMD = "upload"; + @NotNull + private static final String SOURCES_CMD = "sources"; + @NotNull + private static final String UPDATE_CMD = "update"; + @NotNull + private static final String DOWNLOAD_CMD = "download"; + @NotNull + private static final String EXIT_CMD = "exit"; + + @NotNull + private static final String COMMANDS = String.format( + "- %s\n" + + "- %s \n" + + "- %s \n" + + "- %s\n" + + "- %s \n" + + "- %s", + LIST_CMD, UPLOAD_CMD, SOURCES_CMD, UPDATE_CMD, DOWNLOAD_CMD, EXIT_CMD + ); + + private TorrentClientMain() { + } + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println(USAGE); + return; + } + + final short port = Short.parseShort(args[0]); + final Scanner scanner = new Scanner(System.in); + try (TorrentClient client = new TorrentClient(port, Paths.get("."))) { + System.out.println("Hello!"); + while (true) { + try { + final String command = scanner.next(); + switch (command) { + case LIST_CMD: { + final List fileInfos = client.executeList(); + System.out.println("Files on tracker: " + fileInfos.size()); + fileInfos.forEach(System.out::println); + break; + } + case UPLOAD_CMD: { + final File file = Paths.get(scanner.next()).toFile(); + final int fileId = client.executeUpload(file); + System.out.println("Success! New file ID: " + fileId); + break; + } + case SOURCES_CMD: { + final int fileId = scanner.nextInt(); + final Set
addresses = client.executeSources(fileId); + System.out.println("Users who have this file: " + addresses.size()); + addresses.forEach(System.out::println); + break; + } + case UPDATE_CMD: { + final boolean result = client.executeUpdate(); + System.out.println(result ? "Success!" : "Fail?"); + break; + } + case DOWNLOAD_CMD: { + final int fileId = scanner.nextInt(); + client.executeDownload(fileId); + break; + } + case EXIT_CMD: { + System.out.println("Bye-bye!"); + System.exit(0); // wtf + } + default: { + System.err.println("Unknown command: " + command); + System.out.println(COMMANDS); + } + } + } catch (IOException e) { + System.err.println(e.getMessage()); + } + } + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/client/src/main/java/org/itmo/torrent/client/TorrentClientState.java b/client/src/main/java/org/itmo/torrent/client/TorrentClientState.java new file mode 100644 index 0000000..38f7a0c --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/client/TorrentClientState.java @@ -0,0 +1,68 @@ +package org.itmo.torrent.client; + +import org.itmo.torrent.State; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.itmo.torrent.filesystem.FileSeedInfo; + +import java.io.*; +import java.util.*; + +public class TorrentClientState implements State { + @NotNull + private static final String STATE_FILE_NAME = ".torrent-client.stt"; + + @NotNull + private final Map fileInfos = new HashMap<>(); + + @NotNull + @Override + public String getStateFileName() { + return STATE_FILE_NAME; + } + + public synchronized void addFileInfo(@NotNull final FileSeedInfo fileInfo) { + fileInfos.put(fileInfo.getId(), fileInfo); + } + + @Nullable + public FileSeedInfo getFileInfo(final int fileId) { + return fileInfos.get(fileId); + } + + @NotNull + public synchronized Set getFilesIds() { + return fileInfos.keySet(); + } + + @Override + public synchronized void toOutputStream(@NotNull final OutputStream out) throws IOException { + try (ObjectOutputStream objOut = new ObjectOutputStream(out)) { + final Collection fileInfos = this.fileInfos.values(); + objOut.writeInt(fileInfos.size()); + for (FileSeedInfo fileInfo : fileInfos) { + objOut.writeInt(fileInfo.getId()); + objOut.writeUTF(fileInfo.getName()); + objOut.writeLong(fileInfo.getSize()); + objOut.writeUTF(fileInfo.getFile().getAbsolutePath()); + objOut.writeObject(fileInfo.getAvailableChunks()); + } + } + } + + @Override + public synchronized void fromInputStream(@NotNull final InputStream in) + throws IOException, ClassNotFoundException { + try (ObjectInputStream objIn = new ObjectInputStream(in)) { + final int count = objIn.readInt(); + for (int i = 0; i < count; i++) { + final int fileId = objIn.readInt(); + final String fileName = objIn.readUTF(); + final long fileSize = objIn.readLong(); + final File file = new File(objIn.readUTF()); + final BitSet availableChunks = (BitSet) objIn.readObject(); + fileInfos.put(fileId, new FileSeedInfo(fileId, fileName, fileSize, file, availableChunks)); + } + } + } +} diff --git a/client/src/main/java/org/itmo/torrent/filesystem/FileSeedInfo.java b/client/src/main/java/org/itmo/torrent/filesystem/FileSeedInfo.java new file mode 100644 index 0000000..d32cdca --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/filesystem/FileSeedInfo.java @@ -0,0 +1,101 @@ +package org.itmo.torrent.filesystem; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.BitSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class FileSeedInfo extends FileInfo { + @NotNull + private final File file; + @NotNull + private final BitSet availableChunks; + + public FileSeedInfo(final int id, @NotNull final String name, final long size, + @NotNull final File file, @NotNull final BitSet availableChunks) { + super(id, name, size); + this.file = file; + this.availableChunks = availableChunks; + } + + public FileSeedInfo(final int id, @NotNull final String name, final long size, + @NotNull final File file) { + super(id, name, size); + this.file = file; + final int chunksNumber = (int) getChunksNumber(); + availableChunks = new BitSet(chunksNumber); + availableChunks.set(0, chunksNumber); + } + + public FileSeedInfo(@NotNull final FileInfo fileInfo) throws IOException { + super(fileInfo.getId(), fileInfo.getName(), fileInfo.getSize()); + this.file = createFile(fileInfo); + final int chunksNumber = (int) getChunksNumber(); + availableChunks = new BitSet(chunksNumber); + } + + @NotNull + public File getFile() { + return file; + } + + @NotNull + public BitSet getAvailableChunks() { + return availableChunks; + } + + @NotNull + public Set getSetAvailableChunks() { + return IntStream.range(0, availableChunks.length()) + .filter(this::isChunkAvailable) + .boxed() + .collect(Collectors.toSet()); + } + + public boolean isChunkAvailable(final int chunkId) { + return availableChunks.get(chunkId); + } + + public void setChunkAvailable(final int chunkId) { + availableChunks.set(chunkId); + } + + public long getChunksNumber() { + return (getSize() + FILE_CHUNK_SIZE - 1) / FILE_CHUNK_SIZE; + } + + public long getChunkSize(final int chunkId) { + if (chunkId < getChunksNumber() - 1 || getSize() % FILE_CHUNK_SIZE == 0) { + return FILE_CHUNK_SIZE; + } else { + return getSize() % FILE_CHUNK_SIZE; + } + } + + @NotNull + private File createFile(@NotNull FileInfo fileInfo) throws IOException { + final FileSystemManager manager = FileSystemManager.getInstance(); + final File downloadsFolder = manager.getDownloadsFolderPath().toFile(); + + if (!downloadsFolder.exists() && !downloadsFolder.mkdirs()) { + throw new IllegalArgumentException( + String.format("Cannot create directory %s", downloadsFolder.getPath())); + } + + if (!downloadsFolder.isDirectory()) { + throw new IllegalArgumentException( + String.format("%s is not a directory", downloadsFolder.getPath())); + } + + final File file = manager.getDownloadsFolderPath().resolve(fileInfo.getName()).toFile(); + if (!file.exists() && !file.createNewFile()) { + throw new IllegalArgumentException( + String.format("Cannot create file %s", file.getPath())); + } + return file; + } +} diff --git a/client/src/main/java/org/itmo/torrent/filesystem/FileSystemManager.java b/client/src/main/java/org/itmo/torrent/filesystem/FileSystemManager.java new file mode 100644 index 0000000..bdcd175 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/filesystem/FileSystemManager.java @@ -0,0 +1,41 @@ +package org.itmo.torrent.filesystem; + +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileSystemManager { + @NotNull + private static final String DOWNLOADS_FOLDER_NAME = "downloads"; + + @NotNull + private Path rootFolderPath = Paths.get("."); + + private FileSystemManager() { + } + + @NotNull + public Path getRootFolderPath() { + return rootFolderPath; + } + + public void setRootFolderPath(@NotNull final Path rootDirPath) { + this.rootFolderPath = rootDirPath; + } + + @NotNull + public Path getDownloadsFolderPath() { + return rootFolderPath.resolve(DOWNLOADS_FOLDER_NAME); + } + + @NotNull + public static FileSystemManager getInstance() { + return SingletonHolder.instance; + } + + private static class SingletonHolder { + @NotNull + private static final FileSystemManager instance = new FileSystemManager(); + } +} diff --git a/client/src/main/java/org/itmo/torrent/network/Client2PeerConnection.java b/client/src/main/java/org/itmo/torrent/network/Client2PeerConnection.java new file mode 100644 index 0000000..7772142 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/network/Client2PeerConnection.java @@ -0,0 +1,50 @@ +package org.itmo.torrent.network; + +import org.apache.commons.io.input.BoundedInputStream; +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.network.messages.client.Get; +import org.itmo.torrent.network.messages.client.Stat; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.util.HashSet; +import java.util.Set; + +public class Client2PeerConnection extends Connection { + + public Client2PeerConnection(@NotNull final Socket socket) throws IOException { + super(socket); + } + + public void sendStatRequest(@NotNull final Stat.Request message) throws IOException { + out.writeByte(Stat.ID); + out.writeLong(message.getFileId()); + out.flush(); + } + + public void sendGetRequest(@NotNull final Get.Request message) throws IOException { + out.writeByte(Get.ID); + out.writeInt(message.getFileId()); + out.writeInt(message.getFileChunkId()); + out.flush(); + } + + @NotNull + public Stat.Response receiveStatResponse() throws IOException { + final int count = in.readInt(); + final Set filePartsIds = new HashSet() {{ + for (int i = 0; i < count; i++) { + add(in.readInt()); + } + }}; + return new Stat.Response(filePartsIds); + } + + @NotNull + public Get.Response receiveGetResponse() throws IOException { + final int count = in.readInt(); + final InputStream filePartContent = count == -1 ? null : new BoundedInputStream(in, count); + return new Get.Response(filePartContent, count); + } +} diff --git a/client/src/main/java/org/itmo/torrent/network/Client2TrackerConnection.java b/client/src/main/java/org/itmo/torrent/network/Client2TrackerConnection.java new file mode 100644 index 0000000..0f3e6bc --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/network/Client2TrackerConnection.java @@ -0,0 +1,99 @@ +package org.itmo.torrent.network; + +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.messages.tracker.List; +import org.itmo.torrent.network.messages.tracker.Sources; +import org.itmo.torrent.network.messages.tracker.Update; +import org.itmo.torrent.network.messages.tracker.Upload; + +import java.io.IOException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class Client2TrackerConnection extends Connection { + + public Client2TrackerConnection(@NotNull final Socket socket) throws IOException { + super(socket); + } + + public void sendListRequest(@NotNull final List.Request message) throws IOException { + out.writeByte(List.Request.ID); + out.flush(); + } + + public void sendUploadRequest(@NotNull final Upload.Request message) throws IOException { + out.writeByte(Upload.ID); + out.writeUTF(message.getFileName()); + out.writeLong(message.getFileSize()); + out.flush(); + } + + public void sendSourcesRequest(@NotNull final Sources.Request message) throws IOException { + out.writeByte(Sources.ID); + out.writeInt(message.getFileId()); + out.flush(); + } + + public void sendUpdateRequest(@NotNull final Update.Request message) throws IOException { + out.writeByte(Update.ID); + out.writeShort(message.getClientPort()); + out.writeInt(message.getFilesIds().size()); + for (int fileId : message.getFilesIds()) { + out.writeInt(fileId); + } + out.flush(); + } + + @NotNull + public List.Response receiveListResponse() throws IOException { + final int count = in.readInt(); + final java.util.List fileInfos = new ArrayList() {{ + for (int i = 0; i < count; i++) { + add(receiveFileInfo()); + } + }}; + return new List.Response(fileInfos); + } + + @NotNull + public Upload.Response receiveUploadResponse() throws IOException { + final int fileId = in.readInt(); + return new Upload.Response(fileId); + } + + @NotNull + public Sources.Response receiveSourcesResponse() throws IOException { + final int count = in.readInt(); + final Set
clientsAddresses = new HashSet
() {{ + for (int i = 0; i < count; i++) { + add(receiveAddress()); + } + }}; + return new Sources.Response(clientsAddresses); + } + + @NotNull + public Update.Response receiveUpdateResponse() throws IOException { + final boolean status = in.readBoolean(); + return new Update.Response(status); + } + + @NotNull + private FileInfo receiveFileInfo() throws IOException { + final int id = in.readInt(); + final String name = in.readUTF(); + final long size = in.readLong(); + return new FileInfo(id, name, size); + } + + @NotNull + private Address receiveAddress() throws IOException { + final byte[] ip = new byte[4]; + in.readFully(ip); + final short port = in.readShort(); + return new Address(ip, port); + } +} diff --git a/client/src/main/java/org/itmo/torrent/network/Peer2ClientConnection.java b/client/src/main/java/org/itmo/torrent/network/Peer2ClientConnection.java new file mode 100644 index 0000000..00dac69 --- /dev/null +++ b/client/src/main/java/org/itmo/torrent/network/Peer2ClientConnection.java @@ -0,0 +1,45 @@ +package org.itmo.torrent.network; + +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.network.messages.client.Get; +import org.itmo.torrent.network.messages.client.Stat; + +import java.io.IOException; +import java.net.Socket; + +public class Peer2ClientConnection extends Connection { + + public Peer2ClientConnection(@NotNull final Socket socket) throws IOException { + super(socket); + } + + public void sendStatResponse(@NotNull final Stat.Response message) throws IOException { + out.writeInt(message.getFileChunksIds().size()); + for (int filePartId : message.getFileChunksIds()) { + out.writeInt(filePartId); + } + out.flush(); + } + + public void sendGetResponse(@NotNull final Get.Response message) throws IOException { + out.writeInt(message.getCount()); + if (message.getFileChunkContent() != null) { + IOUtils.copy(message.getFileChunkContent(), out); + } + out.flush(); + } + + @NotNull + public Stat.Request receiveStatRequest() throws IOException { + final int fileId = in.readInt(); + return new Stat.Request(fileId); + } + + @NotNull + public Get.Request receiveGetRequest() throws IOException { + final int fileId = in.readInt(); + final int filePartId = in.readInt(); + return new Get.Request(fileId, filePartId); + } +} diff --git a/commons/build.gradle b/commons/build.gradle new file mode 100644 index 0000000..b647301 --- /dev/null +++ b/commons/build.gradle @@ -0,0 +1,14 @@ +group 'org.itmo' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' +} diff --git a/commons/src/main/java/org/itmo/torrent/AbstractServer.java b/commons/src/main/java/org/itmo/torrent/AbstractServer.java new file mode 100644 index 0000000..7a4dcca --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/AbstractServer.java @@ -0,0 +1,47 @@ +package org.itmo.torrent; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public abstract class AbstractServer implements Server, AutoCloseable { + private final short port; + @NotNull + private final ExecutorService service = Executors.newCachedThreadPool(); + @NotNull + private final ServerSocket serverSocket; + + public AbstractServer(final short port) throws IOException { + this.port = port; + serverSocket = new ServerSocket(port); + service.submit(new ConnectionsListener()); + } + + public short getPort() { + return port; + } + + public void close() throws IOException { + serverSocket.close(); + service.shutdown(); + } + + protected abstract Runnable getConnectionHandler(@NotNull final Socket clientSocket); + + private class ConnectionsListener implements Runnable { + @Override + public void run() { + while (!Thread.interrupted()) { + try { + final Socket clientSocket = serverSocket.accept(); + service.submit(getConnectionHandler(clientSocket)); + } catch (IOException ignore) { + } + } + } + } +} diff --git a/commons/src/main/java/org/itmo/torrent/Constants.java b/commons/src/main/java/org/itmo/torrent/Constants.java new file mode 100644 index 0000000..94252fb --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/Constants.java @@ -0,0 +1,12 @@ +package org.itmo.torrent; + +import org.jetbrains.annotations.NotNull; + +public class Constants { + @NotNull + public static final String TRACKER_HOST = "localhost"; + public static final short TRACKER_PORT = 8081; + + private Constants() { + } +} diff --git a/commons/src/main/java/org/itmo/torrent/SeedInfo.java b/commons/src/main/java/org/itmo/torrent/SeedInfo.java new file mode 100644 index 0000000..5dbaaef --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/SeedInfo.java @@ -0,0 +1,52 @@ +package org.itmo.torrent; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.itmo.torrent.network.Address; +import org.itmo.torrent.network.messages.tracker.Update; + +import java.util.Objects; + +public class SeedInfo { + @NotNull + private final Address address; + + private volatile long lastUpdate ; + + public SeedInfo(@NotNull final Address address) { + this.address = address; + update(); + } + + public void update() { + lastUpdate = System.currentTimeMillis(); + } + + @NotNull + public Address getAddress() { + return address; + } + + public boolean isExpired() { + return System.currentTimeMillis() - lastUpdate > Update.UPDATE_PERIOD * 2; + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + final SeedInfo seedInfo = (SeedInfo) object; + return Objects.equals(getAddress(), seedInfo.getAddress()); + } + + @Override + public int hashCode() { + return Objects.hash(getAddress()); + } +} diff --git a/commons/src/main/java/org/itmo/torrent/Server.java b/commons/src/main/java/org/itmo/torrent/Server.java new file mode 100644 index 0000000..4d82334 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/Server.java @@ -0,0 +1,5 @@ +package org.itmo.torrent; + +public interface Server { + // TODO: interface? +} diff --git a/commons/src/main/java/org/itmo/torrent/State.java b/commons/src/main/java/org/itmo/torrent/State.java new file mode 100644 index 0000000..0532d43 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/State.java @@ -0,0 +1,61 @@ +package org.itmo.torrent; + +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.nio.file.Path; + +public interface State { + + default void save(@NotNull final Path rootFolderPath) throws IOException { + final File rootFolder = rootFolderPath.toFile(); + + if (!rootFolder.exists() && !rootFolder.mkdirs()) { + throw new IllegalArgumentException( + String.format("Cannot create directory %s", rootFolderPath)); + } + + if (!rootFolder.isDirectory()) { + throw new IllegalArgumentException( + String.format("%s is not a directory", rootFolderPath)); + } + + final Path stateFilePath = rootFolderPath.resolve(getStateFileName()); + final File stateFile = stateFilePath.toFile(); + + if (!stateFile.exists() && !stateFile.createNewFile()) { + throw new IllegalArgumentException( + String.format("Cannot create state file %s", stateFilePath)); + } + + try (OutputStream out = new FileOutputStream(stateFile)) { + toOutputStream(out); + } + } + + default void restore(@NotNull final Path rootFolderPath) + throws IOException, ClassNotFoundException { + final Path stateFilePath = rootFolderPath.resolve(getStateFileName()); + final File stateFile = stateFilePath.toFile(); + + if (!stateFile.exists()) { + return; + } + + if (!stateFile.isFile()) { + throw new IllegalArgumentException( + String.format("%s is not a regular file", stateFilePath)); + } + + try (InputStream in = new FileInputStream(stateFile)) { + fromInputStream(in); + } + } + + @NotNull + String getStateFileName(); + + void toOutputStream(@NotNull final OutputStream out) throws IOException; + + void fromInputStream(@NotNull final InputStream in) throws IOException, ClassNotFoundException; +} diff --git a/commons/src/main/java/org/itmo/torrent/filesystem/FileInfo.java b/commons/src/main/java/org/itmo/torrent/filesystem/FileInfo.java new file mode 100644 index 0000000..823ecbb --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/filesystem/FileInfo.java @@ -0,0 +1,62 @@ +package org.itmo.torrent.filesystem; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class FileInfo { + public final static long FILE_CHUNK_SIZE = 10 * 1024; + + private final int id; + + @NotNull + private final String name; + + private final long size; + + public FileInfo(final int id, @NotNull final String name, final long size) { + this.id = id; + this.name = name; + this.size = size; + } + + public int getId() { + return id; + } + + @NotNull + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + @Override + public String toString() { + return String.format("FileInfo{id=%d, name=%s, size=%d}", id, name, size); + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + final FileInfo fileInfo = (FileInfo) object; + return getId() == fileInfo.getId() && + getSize() == fileInfo.getSize() && + Objects.equals(getName(), fileInfo.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getId(), getName(), getSize()); + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/Address.java b/commons/src/main/java/org/itmo/torrent/network/Address.java new file mode 100644 index 0000000..c75dad1 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/Address.java @@ -0,0 +1,63 @@ +package org.itmo.torrent.network; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Objects; + +public class Address { + public static final int IP_SIZE = 4; + + @NotNull + private final byte[] ip; + private final short port; + + public Address(@NotNull final byte[] ip, final short port) { + if (ip.length != IP_SIZE) { + throw new IllegalArgumentException("Invalid IP size"); + } + + this.ip = ip; + this.port = port; + } + + @NotNull + public byte[] getIp() { + return ip; + } + + @NotNull + public String getHost() { + return String.format("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + } + + public short getPort() { + return port; + } + + @NotNull + @Override + public String toString() { + return String.format("Address{ip=%s, port=%d)", getHost(), getPort()); + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + final Address address = (Address) object; + return getPort() == address.getPort() && Arrays.equals(getIp(), address.getIp()); + } + + @Override + public int hashCode() { + return 31 * Objects.hash(getPort()) + Arrays.hashCode(getIp()); + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/Connection.java b/commons/src/main/java/org/itmo/torrent/network/Connection.java new file mode 100644 index 0000000..8f1a719 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/Connection.java @@ -0,0 +1,32 @@ +package org.itmo.torrent.network; + +import org.jetbrains.annotations.NotNull; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; + +public abstract class Connection implements AutoCloseable { + @NotNull + protected final DataInputStream in; + @NotNull + protected final DataOutputStream out; + @NotNull + private final Socket socket; + + public Connection(@NotNull final Socket socket) throws IOException { + this.socket = socket; + in = new DataInputStream(socket.getInputStream()); + out = new DataOutputStream(socket.getOutputStream()); + } + + public byte receiveRequestType() throws IOException { + return in.readByte(); + } + + @Override + public void close() throws IOException { +// socket.close(); // TODO: really? + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/Message.java b/commons/src/main/java/org/itmo/torrent/network/messages/Message.java new file mode 100644 index 0000000..8ab3025 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/Message.java @@ -0,0 +1,5 @@ +package org.itmo.torrent.network.messages; + +public interface Message { + // TODO: interface? +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/client/Get.java b/commons/src/main/java/org/itmo/torrent/network/messages/client/Get.java new file mode 100644 index 0000000..f2d3450 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/client/Get.java @@ -0,0 +1,61 @@ +package org.itmo.torrent.network.messages.client; + +import org.itmo.torrent.network.messages.Message; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.InputStream; + +public interface Get extends Message { + byte ID = 2; + + class Request implements Get { + private final int fileId; + private final int fileChunkId; + + public Request(final int fileId, final int fileChunkId) { + this.fileId = fileId; + this.fileChunkId = fileChunkId; + } + + public int getFileId() { + return fileId; + } + + public int getFileChunkId() { + return fileChunkId; + } + + @NotNull + @Override + public String toString() { + return String.format("GetRequest{fileId=%d, fileChunkId=%d}", fileId, fileChunkId); + } + } + + class Response implements Get { + @Nullable + private final InputStream fileChunkContent; + private final int count; + + public Response(@Nullable final InputStream fileChunkContent, final int count) { + this.fileChunkContent = fileChunkContent; + this.count = count; + } + + @Nullable + public InputStream getFileChunkContent() { + return fileChunkContent; + } + + public int getCount() { + return count; + } + + @NotNull + @Override + public String toString() { + return String.format("GetResponse{fileChunkContent=..., count=%d}", count); + } + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/client/Stat.java b/commons/src/main/java/org/itmo/torrent/network/messages/client/Stat.java new file mode 100644 index 0000000..4ede19c --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/client/Stat.java @@ -0,0 +1,49 @@ +package org.itmo.torrent.network.messages.client; + +import org.itmo.torrent.network.messages.Message; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public interface Stat extends Message { + byte ID = 1; + + class Request implements Stat { + private final int fileId; + + public Request(final int fileId) { + this.fileId = fileId; + } + + public int getFileId() { + return fileId; + } + + @NotNull + @Override + public String toString() { + return String.format("StatRequest{fileId=%d}", fileId); + } + } + + class Response implements Stat { + @NotNull + private final Set fileChunksIds; + + public Response(@NotNull final Set fileChunksIds) { + this.fileChunksIds = fileChunksIds; + } + + @NotNull + public Set getFileChunksIds() { + return fileChunksIds; + } + + @NotNull + @Override + public String toString() { + return String.format("StatResponse{count=%d, fileChunksIds=%s}", + fileChunksIds.size(), fileChunksIds); + } + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/tracker/List.java b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/List.java new file mode 100644 index 0000000..b7f138e --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/List.java @@ -0,0 +1,37 @@ +package org.itmo.torrent.network.messages.tracker; + +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.messages.Message; +import org.jetbrains.annotations.NotNull; + +public interface List extends Message { + byte ID = 1; + + class Request implements List { + @NotNull + @Override + public String toString() { + return "ListRequest{}"; + } + } + + class Response implements List { + @NotNull + private final java.util.List fileInfos; + + public Response(@NotNull final java.util.List fileInfos) { + this.fileInfos = fileInfos; + } + + @NotNull + public java.util.List getFileInfos() { + return fileInfos; + } + + @NotNull + @Override + public String toString() { + return String.format("ListResponse{count=%d, fileInfos=%s}", fileInfos.size(), fileInfos); + } + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Sources.java b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Sources.java new file mode 100644 index 0000000..6d594e3 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Sources.java @@ -0,0 +1,50 @@ +package org.itmo.torrent.network.messages.tracker; + +import org.itmo.torrent.network.Address; +import org.itmo.torrent.network.messages.Message; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public interface Sources extends Message { + byte ID = 3; + + class Request implements Sources { + private final int fileId; + + public Request(final int fileId) { + this.fileId = fileId; + } + + public int getFileId() { + return fileId; + } + + @NotNull + @Override + public String toString() { + return String.format("SourcesRequest{fileId=%d}", fileId); + } + } + + class Response implements Sources { + @NotNull + private final Set
clientsAddresses; + + public Response(@NotNull final Set
clientsAddresses) { + this.clientsAddresses = clientsAddresses; + } + + @NotNull + public Set
getClientsAddresses() { + return clientsAddresses; + } + + @NotNull + @Override + public String toString() { + return String.format("SourcesResponse{size=%d, clientsAddresses=%s}", + clientsAddresses.size(), clientsAddresses); + } + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Update.java b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Update.java new file mode 100644 index 0000000..a33d4a1 --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Update.java @@ -0,0 +1,57 @@ +package org.itmo.torrent.network.messages.tracker; + +import org.itmo.torrent.network.messages.Message; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public interface Update extends Message { + byte ID = 4; + int UPDATE_PERIOD = 10 * 60; // millis + + class Request implements Update { + private final short clientPort; + + @NotNull + private final Set filesIds; + + public Request(final short clientPort, @NotNull final Set filesIds) { + this.clientPort = clientPort; + this.filesIds = filesIds; + } + + public short getClientPort() { + return clientPort; + } + + @NotNull + public Set getFilesIds() { + return filesIds; + } + + @NotNull + @Override + public String toString() { + return String.format("UpdateRequest{clientPort=%d, count=%d, filesIds=%s}", + clientPort, filesIds.size(), filesIds); + } + } + + class Response implements Update { + private final boolean status; + + public Response(final boolean status) { + this.status = status; + } + + public boolean getStatus() { + return status; + } + + @NotNull + @Override + public String toString() { + return String.format("UpdateResponse{status=%b}", status); + } + } +} diff --git a/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Upload.java b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Upload.java new file mode 100644 index 0000000..4fefa6d --- /dev/null +++ b/commons/src/main/java/org/itmo/torrent/network/messages/tracker/Upload.java @@ -0,0 +1,52 @@ +package org.itmo.torrent.network.messages.tracker; + +import org.itmo.torrent.network.messages.Message; +import org.jetbrains.annotations.NotNull; + +public interface Upload extends Message { + byte ID = 2; + + class Request implements Message { + @NotNull + private final String fileName; + private final long fileSize; + + public Request(@NotNull final String fileName, final long fileSize) { + this.fileName = fileName; + this.fileSize = fileSize; + } + + @NotNull + public String getFileName() { + return fileName; + } + + public long getFileSize() { + return fileSize; + } + + @NotNull + @Override + public String toString() { + return String.format("UploadRequest{fileName=%s, fileSize=%d}", fileName, fileSize); + } + } + + class Response implements Message { + private final int fileId; + + public Response(final int fileId) { + this.fileId = fileId; + } + + public int getFileId() { + return fileId; + } + + @NotNull + @Override + public String toString() { + return String.format("UploadResponse{fileId=%d}", fileId); + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..27768f1 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..92165ee --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f34e9a7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +rootProject.name = 'torrent' +include 'tracker' +include 'client' +include 'commons' diff --git a/src/test/java/org/itmo/torrent/IntegrationTest.java b/src/test/java/org/itmo/torrent/IntegrationTest.java new file mode 100644 index 0000000..5f27b08 --- /dev/null +++ b/src/test/java/org/itmo/torrent/IntegrationTest.java @@ -0,0 +1,272 @@ +package org.itmo.torrent; + +import org.itmo.torrent.tracker.TorrentTracker; +import org.junit.*; +import org.junit.rules.TemporaryFolder; +import org.itmo.torrent.client.TorrentClient; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.Address; +import org.itmo.torrent.network.messages.tracker.Update; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; + +public class IntegrationTest { + private static final short CLIENT1_PORT = 1111; + private static final short CLIENT2_PORT = 2222; + private static final short CLIENT3_PORT = 3333; + + @Rule + public TemporaryFolder trackerFolder = new TemporaryFolder(); + @Rule + public TemporaryFolder client1Folder = new TemporaryFolder(); + @Rule + public TemporaryFolder client2Folder = new TemporaryFolder(); + @Rule + public TemporaryFolder client3Folder = new TemporaryFolder(); + + private TorrentTracker tracker; + private TorrentClient client1; + private TorrentClient client2; + private TorrentClient client3; + + @Before + public void setUp() throws Exception { + tracker = new TorrentTracker(trackerFolder.getRoot().toPath()); + client1 = new TorrentClient(CLIENT1_PORT, client1Folder.getRoot().toPath()); + client2 = new TorrentClient(CLIENT2_PORT, client2Folder.getRoot().toPath()); + client3 = new TorrentClient(CLIENT3_PORT, client3Folder.getRoot().toPath()); + } + + @After + public void tearDown() throws Exception { + if (tracker != null) { + tracker.close(); + } + if (client1 != null) { + client1.close(); + } + if (client2 != null) { + client2.close(); + } + if (client3 != null) { + client3.close(); + } + + Thread.sleep(50); + } + + @Test + public void testListEmpty() throws IOException { + final List expected = Collections.emptyList(); + final List actual = client1.executeList(); + assertEquals(expected, actual); + } + + @Test + public void testUpload1() throws IOException { + final String fileName = "dummy.txt"; + final File file = client1Folder.newFile(fileName); + client1.executeUpload(file); + final List expected = new ArrayList() {{ + add(new FileInfo(0, fileName, 0)); + }}; + final List actual = client1.executeList(); + assertEquals(expected, actual); + } + + @Test + public void testUpload2() throws IOException { + final String file1Name = "dummy1.txt"; + final String file2Name = "dummy2.txt"; + final File file1 = client1Folder.newFile(file1Name); + final File file2 = client2Folder.newFile(file2Name); + client1.executeUpload(file1); + client2.executeUpload(file2); + final Set expected = new HashSet() {{ + add(new FileInfo(0, file1Name, 0)); + add(new FileInfo(1, file2Name, 0)); + }}; + final Set actual1 = new HashSet<>(client1.executeList()); + final Set actual2 = new HashSet<>(client2.executeList()); + assertEquals(expected, actual1); + assertEquals(expected, actual2); + } + + @Test + public void testUploadBigFile() throws IOException { + final String fileName = "dummy1.txt"; + final int newLength = 100000; + uploadBigFile(0); + final List expected = new ArrayList() {{ + add(new FileInfo(0, fileName, newLength)); + }}; + final List actual = client1.executeList(); + assertEquals(expected, actual); + } + + @Test + public void testUploadSameFile() throws IOException { + final String fileName = "dummy.txt"; + final File file = client1Folder.newFile(fileName); + client1.executeUpload(file); + client1.executeUpload(file); + final Set expected = new HashSet() {{ + add(new FileInfo(0, fileName, 0)); + add(new FileInfo(1, fileName, 0)); + }}; + final Set actual = new HashSet<>(client1.executeList()); + assertEquals(expected, actual); + } + + // @Ignore // too long to wait + @Test + public void testUpdateSources() throws IOException, InterruptedException { + final String fileName = "dummy.txt"; + final File file = client1Folder.newFile(fileName); + client1.executeUpload(file); + + Thread.sleep(Update.UPDATE_PERIOD * 2); + + final Set
expected = new HashSet
() {{ + add(new Address(new byte[]{127, 0, 0, 1}, CLIENT1_PORT)); + }}; + final Set
actual = client2.executeSources(0); + assertEquals(expected, actual); + } + + // @Ignore // too long to wait + @Test + public void testUpdateExpire() throws IOException, InterruptedException { + final String fileName = "dummy.txt"; + final File file = client1Folder.newFile(fileName); + client1.executeUpload(file); + + Thread.sleep(Update.UPDATE_PERIOD * 2); + + client1.close(); + + Thread.sleep(Update.UPDATE_PERIOD * 2); + + final Set
expected = Collections.emptySet(); + final Set
actual = client2.executeSources(0); + assertEquals(expected, actual); + } + + @Test + public void testTrackerPersistentState() throws IOException, ClassNotFoundException { + final String fileName = "dummy.txt"; + final File file = client1Folder.newFile(fileName); + client1.executeUpload(file); + + tracker.close(); + tracker = new TorrentTracker(trackerFolder.getRoot().toPath()); + + final List expected = new ArrayList() {{ + add(new FileInfo(0, fileName, 0)); + }}; + final List actual = client2.executeList(); + assertEquals(expected, actual); + } + + @Ignore // too long to wait + @Test + public void testClientPersistentState() + throws IOException, InterruptedException, ClassNotFoundException { + final String fileName = "dummy.txt"; + final File file = client1Folder.newFile(fileName); + client1.executeUpload(file); + client1.close(); + client1 = new TorrentClient(CLIENT1_PORT, client1Folder.getRoot().toPath()); + + Thread.sleep(Update.UPDATE_PERIOD * 5); + + final Set
expected = new HashSet
() {{ + add(new Address(new byte[]{127, 0, 0, 1}, CLIENT1_PORT)); + }}; + final Set
actual = client2.executeSources(0); + assertEquals(expected, actual); + } + + @Test + public void testStat() throws IOException { + final int newLength = 100000; + final int fileId = uploadBigFile(0); + final Set expected = IntStream.range(0, newLength / (int) FileInfo.FILE_CHUNK_SIZE + 1) + .boxed() + .collect(Collectors.toSet()); + final Address address = new Address(new byte[]{127, 0, 0, 1}, CLIENT1_PORT); + final Set actual = client2.executeStat(address, fileId); + assertEquals(expected, actual); + } + + @Test + public void testDownload() throws IOException, InterruptedException { + final int newLength = 100000; + final int fileId = uploadBigFile(0); + Thread.sleep(Update.UPDATE_PERIOD * 2); + final Set expected = IntStream.range(0, newLength / (int) FileInfo.FILE_CHUNK_SIZE + 1) + .boxed() + .collect(Collectors.toSet()); + client2.executeDownload(fileId); + Thread.sleep(100); + Thread.sleep(Update.UPDATE_PERIOD * 2); + final Address address = new Address(new byte[]{127, 0, 0, 1}, CLIENT2_PORT); + final Set actual = client3.executeStat(address, fileId); + assertEquals(expected, actual); + } + + // @Ignore // too long to wait + @Test + public void testDownloadParallel() throws InterruptedException, IOException { + final int newLength = 100000; + final int fileId1 = uploadBigFile(0); + final int fileId2 = uploadBigFile(1); + final int fileId3 = uploadBigFile(2); + Thread.sleep(Update.UPDATE_PERIOD * 2); + final Set expected = IntStream.range(0, newLength / (int) FileInfo.FILE_CHUNK_SIZE + 1) + .boxed() + .collect(Collectors.toSet()); + client1.executeDownload(fileId2); + client1.executeDownload(fileId3); + client2.executeDownload(fileId1); + client2.executeDownload(fileId3); + client3.executeDownload(fileId1); + client3.executeDownload(fileId2); + Thread.sleep(100); + Thread.sleep(Update.UPDATE_PERIOD * 2); + final Address address1 = new Address(new byte[]{127, 0, 0, 1}, CLIENT1_PORT); + final Address address2 = new Address(new byte[]{127, 0, 0, 1}, CLIENT2_PORT); + final Address address3 = new Address(new byte[]{127, 0, 0, 1}, CLIENT3_PORT); + final Set actual1 = client1.executeStat(address2, fileId1); + assertEquals(expected, actual1); + final Set actual2 = client1.executeStat(address2, fileId3); + assertEquals(expected, actual2); + final Set actual3 = client2.executeStat(address3, fileId1); + assertEquals(expected, actual3); + final Set actual4 = client2.executeStat(address3, fileId2); + assertEquals(expected, actual4); + final Set actual5 = client3.executeStat(address1, fileId2); + assertEquals(expected, actual5); + final Set actual6 = client3.executeStat(address1, fileId3); + assertEquals(expected, actual6); + } + + private int uploadBigFile(final int i) throws IOException { + final String[] fileName = {"dummy1.txt", "dummy2.txt", "dummy3.txt"}; + final TemporaryFolder[] clientFolder = {client1Folder, client2Folder, client3Folder}; + final File file = clientFolder[i].newFile(fileName[i]); + final int newLength = 100000; + try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) { + randomAccessFile.setLength(newLength); + } + final TorrentClient[] client = {client1, client2, client3}; + return client[i].executeUpload(file); + } +} diff --git a/tracker/build.gradle b/tracker/build.gradle new file mode 100644 index 0000000..9f125ea --- /dev/null +++ b/tracker/build.gradle @@ -0,0 +1,28 @@ +group 'org.itmo' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jar { + from { + configurations.compile.collect { + it.isDirectory() ? it : zipTree(it) + } + } + manifest { + attributes 'Main-Class': 'org.itmo.torrent.tracker.TorrentTrackerMain' + } +} + +dependencies { + compile project(':commons') + compile group: 'commons-io', name: 'commons-io', version: '2.6' + compile group: 'org.jetbrains', name: 'annotations', version: '15.0' + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/tracker/src/main/java/org/itmo/torrent/network/Tracker2ClientConnection.java b/tracker/src/main/java/org/itmo/torrent/network/Tracker2ClientConnection.java new file mode 100644 index 0000000..3106d36 --- /dev/null +++ b/tracker/src/main/java/org/itmo/torrent/network/Tracker2ClientConnection.java @@ -0,0 +1,87 @@ +package org.itmo.torrent.network; + +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.messages.tracker.List; +import org.itmo.torrent.network.messages.tracker.Sources; +import org.itmo.torrent.network.messages.tracker.Update; +import org.itmo.torrent.network.messages.tracker.Upload; + +import java.io.IOException; +import java.net.Socket; +import java.util.HashSet; +import java.util.Set; + +public class Tracker2ClientConnection extends Connection { + + public Tracker2ClientConnection(@NotNull final Socket socket) throws IOException { + super(socket); + } + + public void sendListResponse(@NotNull final List.Response message) throws IOException { + out.writeInt(message.getFileInfos().size()); + for (FileInfo fileInfo : message.getFileInfos()) { + sendFileInfo(fileInfo); + } + out.flush(); + } + + public void sendUploadResponse(@NotNull final Upload.Response message) throws IOException { + out.writeLong(message.getFileId()); + out.flush(); + } + + public void sendSourcesResponse(@NotNull final Sources.Response message) throws IOException { + out.writeInt(message.getClientsAddresses().size()); + for (Address clientAddress : message.getClientsAddresses()) { + sendAddress(clientAddress); + } + out.flush(); + } + + public void sendUpdateResponse(@NotNull final Update.Response message) throws IOException { + out.writeBoolean(message.getStatus()); + out.flush(); + } + + @NotNull + public List.Request receiveListRequest() { + return new List.Request(); + } + + @NotNull + public Upload.Request receiveUploadRequest() throws IOException { + final String fileName = in.readUTF(); + final long fileSize = in.readLong(); + return new Upload.Request(fileName, fileSize); + } + + @NotNull + public Sources.Request receiveSourcesRequest() throws IOException { + final int fileId = in.readInt(); + return new Sources.Request(fileId); + } + + @NotNull + public Update.Request receiveUpdateRequest() throws IOException { + final short clientPort = in.readShort(); + final int count = in.readInt(); + final Set filesIds = new HashSet() {{ + for (int i = 0; i < count; i++) { + add(in.readInt()); + } + }}; + return new Update.Request(clientPort, filesIds); + } + + private void sendFileInfo(@NotNull final FileInfo fileInfo) throws IOException { + out.writeInt(fileInfo.getId()); + out.writeUTF(fileInfo.getName()); + out.writeLong(fileInfo.getSize()); + } + + private void sendAddress(@NotNull final Address address) throws IOException { + out.write(address.getIp()); + out.writeShort(address.getPort()); + } +} diff --git a/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTracker.java b/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTracker.java new file mode 100644 index 0000000..51bd873 --- /dev/null +++ b/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTracker.java @@ -0,0 +1,111 @@ +package org.itmo.torrent.tracker; + +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.AbstractServer; +import org.itmo.torrent.Constants; +import org.itmo.torrent.network.Address; +import org.itmo.torrent.network.Tracker2ClientConnection; +import org.itmo.torrent.network.messages.tracker.List; +import org.itmo.torrent.network.messages.tracker.Sources; +import org.itmo.torrent.network.messages.tracker.Update; +import org.itmo.torrent.network.messages.tracker.Upload; + +import java.io.IOException; +import java.net.Socket; +import java.nio.file.Path; +import java.util.Set; + +public class TorrentTracker extends AbstractServer { + @NotNull + private final Path rootFolderPath; + @NotNull + private final TorrentTrackerState state = new TorrentTrackerState(); + + public TorrentTracker(@NotNull final Path rootFolderPath) + throws IOException, ClassNotFoundException { + super(Constants.TRACKER_PORT); + this.rootFolderPath = rootFolderPath; + state.restore(rootFolderPath); + } + + public void close() throws IOException { + super.close(); + state.save(rootFolderPath); + } + + @NotNull + @Override + protected Runnable getConnectionHandler(@NotNull final Socket clientSocket) { + return new ConnectionHandler(clientSocket); + } + + private class ConnectionHandler implements Runnable { + @NotNull + final Socket socket; + + public ConnectionHandler(@NotNull final Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + try (Tracker2ClientConnection connection = new Tracker2ClientConnection(socket)) { + int requestType = connection.receiveRequestType(); + switch (requestType) { + case List.ID: + handleListRequest(connection); + break; + case Upload.ID: + handleUploadRequest(connection); + break; + case Sources.ID: + handleSourcesRequest(connection); + break; + case Update.ID: + handleUpdateRequest(connection); + break; + default: + throw new IOException("Unknown request type"); + } + } catch (Exception e) { + System.err.println(e.getMessage()); + } + } + + private void handleListRequest(@NotNull final Tracker2ClientConnection connection) + throws IOException { +// final List.Request request = connection.receiveListRequest(); + final List.Response response = new List.Response(state.getFileInfos()); + connection.sendListResponse(response); + } + + private void handleUploadRequest(@NotNull final Tracker2ClientConnection connection) + throws IOException { + final Upload.Request request = connection.receiveUploadRequest(); + final int fileId = state.addNewFile(request.getFileName(), request.getFileSize()); + final Upload.Response response = new Upload.Response(fileId); + connection.sendUploadResponse(response); + } + + private void handleSourcesRequest(@NotNull final Tracker2ClientConnection connection) + throws IOException { + final Sources.Request request = connection.receiveSourcesRequest(); + final int fileId = request.getFileId(); + final Set
seederAddresses = state.getSeeders(fileId); + final Sources.Response response = new Sources.Response(seederAddresses); + connection.sendSourcesResponse(response); + } + + private void handleUpdateRequest(@NotNull final Tracker2ClientConnection connection) + throws IOException { + final Update.Request request = connection.receiveUpdateRequest(); + final Address seederPort = new Address(socket.getInetAddress().getAddress(), + request.getClientPort()); + for (int fileId : request.getFilesIds()) { + state.addSeeder(fileId, seederPort); + } + final Update.Response response = new Update.Response(true); // TODO: false? + connection.sendUpdateResponse(response); + } + } +} diff --git a/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTrackerMain.java b/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTrackerMain.java new file mode 100644 index 0000000..4a0488b --- /dev/null +++ b/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTrackerMain.java @@ -0,0 +1,19 @@ +package org.itmo.torrent.tracker; + +import java.nio.file.Paths; + +public class TorrentTrackerMain { + + private TorrentTrackerMain() { + } + + public static void main(String[] args) { + try (TorrentTracker tracker = new TorrentTracker(Paths.get("."))) { + System.out.println("Press Enter key to shutdown..."); + System.in.read(); + System.exit(0); // wtf + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTrackerState.java b/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTrackerState.java new file mode 100644 index 0000000..ee904d1 --- /dev/null +++ b/tracker/src/main/java/org/itmo/torrent/tracker/TorrentTrackerState.java @@ -0,0 +1,78 @@ +package org.itmo.torrent.tracker; + +import org.jetbrains.annotations.NotNull; +import org.itmo.torrent.SeedInfo; +import org.itmo.torrent.State; +import org.itmo.torrent.filesystem.FileInfo; +import org.itmo.torrent.network.Address; + +import java.io.*; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +public class TorrentTrackerState implements State { + @NotNull + private static final String STATE_FILE_NAME = ".torrent-tracker.stt"; + + @NotNull + private final Map fileInfos = new HashMap<>(); + @NotNull + private final Map> fileSeeders = new HashMap<>(); + + @NotNull + @Override + public String getStateFileName() { + return STATE_FILE_NAME; + } + + public synchronized int addNewFile(@NotNull final String fileName, final long fileSize) { + final FileInfo fileInfo = new FileInfo(fileInfos.size(), fileName, fileSize); + fileInfos.putIfAbsent(fileInfo.getId(), fileInfo); + return fileInfo.getId(); + } + + @NotNull + public synchronized List getFileInfos() { + return new CopyOnWriteArrayList<>(fileInfos.values()); + } + + public synchronized void addSeeder(final int fileId, @NotNull final Address seederAddress) { + final Set seeders = fileSeeders.computeIfAbsent(fileId, (key) -> new HashSet<>()); + seeders.add(new SeedInfo(seederAddress)); + } + + @NotNull + public synchronized Set
getSeeders(final int fileId) { + return fileSeeders.getOrDefault(fileId, Collections.emptySet()).stream() + .filter(seedInfo -> !seedInfo.isExpired()) + .map(SeedInfo::getAddress) + .collect(Collectors.toSet()); + } + + @Override + public synchronized void toOutputStream(@NotNull final OutputStream out) throws IOException { + try (DataOutputStream dataOut = new DataOutputStream(out)) { + final Collection fileInfos = this.fileInfos.values(); + dataOut.writeInt(fileInfos.size()); + for (FileInfo fileInfo : fileInfos) { + dataOut.writeInt(fileInfo.getId()); + dataOut.writeUTF(fileInfo.getName()); + dataOut.writeLong(fileInfo.getSize()); + } + } + } + + @Override + public synchronized void fromInputStream(@NotNull final InputStream in) throws IOException { + try (DataInputStream dataIn = new DataInputStream(in)) { + final int count = dataIn.readInt(); + for (int i = 0; i < count; i++) { + final int fileId = dataIn.readInt(); + final String fileName = dataIn.readUTF(); + final long fileSize = dataIn.readLong(); + fileInfos.put(fileId, new FileInfo(fileId, fileName, fileSize)); + } + } + } +}