Compare commits

...

100 Commits

Author SHA1 Message Date
Zlatin Balevsky
538eca9297 Release 0.3.6 2019-06-23 08:54:28 +01:00
Zlatin Balevsky
e73a23d4a4 fix space not showing 2019-06-23 08:44:51 +01:00
Zlatin Balevsky
76e41a0383 fix restoring paused downloads 2019-06-23 08:42:45 +01:00
Zlatin Balevsky
7045927666 hide monitor options from gui 2019-06-23 08:02:28 +01:00
Zlatin Balevsky
5fb3086b42 update faq 2019-06-23 07:52:01 +01:00
Zlatin Balevsky
2de18227c1 persist pause state 2019-06-23 07:48:49 +01:00
Zlatin Balevsky
bd12a1de3d pause/resume downloads 2019-06-23 06:59:52 +01:00
Zlatin Balevsky
a3a91050c8 update todo 2019-06-23 01:50:30 +01:00
Zlatin Balevsky
6c1cc28e49 shutdown if connection to I2P router is lost 2019-06-22 17:32:12 +01:00
Zlatin Balevsky
b6e5b54f05 do not show monitor by default 2019-06-22 14:51:26 +01:00
Zlatin Balevsky
a6e559ec67 change some defaults 2019-06-22 06:54:49 +01:00
Zlatin Balevsky
f11badb824 update todo 2019-06-21 22:43:46 +01:00
Zlatin Balevsky
44da44ff6f Release 0.3.5 2019-06-21 22:35:54 +01:00
Zlatin Balevsky
aae3fc29ca add logging.properties with various degree of noisiness 2019-06-21 22:28:57 +01:00
Zlatin Balevsky
c30aa19d8b Merge branch 'download-mesh' 2019-06-21 22:26:17 +01:00
Zlatin Balevsky
c79e8712d0 correctly determine if uploader has requested piece 2019-06-21 20:36:33 +01:00
Zlatin Balevsky
ed12d78a48 clear pieces on cancel 2019-06-21 17:22:55 +01:00
Zlatin Balevsky
d27872cc8b investigate StringIndexOutOfBounds 2019-06-21 16:29:52 +01:00
Zlatin Balevsky
f794c39760 personas not destinations 2019-06-21 16:15:35 +01:00
Zlatin Balevsky
2be9c425f7 compute which pieces are requested 2019-06-21 16:09:57 +01:00
Zlatin Balevsky
ab5fea9216 416 if piece not downloaded 2019-06-21 16:03:20 +01:00
Zlatin Balevsky
d1c8328080 do not send alts if there aren't any 2019-06-21 15:39:00 +01:00
Zlatin Balevsky
89e761f53b write personas on the wire part1 2019-06-21 15:26:18 +01:00
Zlatin Balevsky
40410eba63 fix constructor 2019-06-21 14:57:53 +01:00
Zlatin Balevsky
85466a8e80 fix npe 2019-06-21 14:45:14 +01:00
Zlatin Balevsky
c210af7870 source partial uploads from incompletes file 2019-06-21 14:39:20 +01:00
Zlatin Balevsky
38ff49d28f downloaders get pieces from mesh manager 2019-06-21 14:17:10 +01:00
Zlatin Balevsky
710f9f52a8 send X-Have and X-Alts from uploader 2019-06-21 13:58:21 +01:00
Zlatin Balevsky
1b6eda5a40 skeleton of mesh manager 2019-06-21 13:34:00 +01:00
Zlatin Balevsky
1ee9ccf098 parse X-Have on uploader side 2019-06-21 12:55:25 +01:00
Zlatin Balevsky
0f07562de3 pass new sources to active downloaders 2019-06-21 12:39:16 +01:00
Zlatin Balevsky
6eb1aa07f5 key downloaders by infohash 2019-06-21 12:29:32 +01:00
Zlatin Balevsky
05b02834af parse X-Alt 2019-06-21 12:25:04 +01:00
Zlatin Balevsky
56125f6df8 refactor X-Have decoding logic 2019-06-21 09:32:10 +01:00
Zlatin Balevsky
8f9996848b send X-Have from downloader too 2019-06-21 09:25:28 +01:00
Zlatin Balevsky
dd655ed60f test for re-requesting available pieces 2019-06-21 09:12:42 +01:00
Zlatin Balevsky
8923c6ff7d exclude local results by default 2019-06-21 08:15:20 +01:00
Zlatin Balevsky
807ab22f8e test parsing of X-Have 2019-06-21 06:43:48 +01:00
Zlatin Balevsky
a26ad229ee more tests 2019-06-21 05:56:42 +01:00
Zlatin Balevsky
5504dd2251 tighten conditions 2019-06-21 05:45:11 +01:00
Zlatin Balevsky
f9777d29f4 get existing tests to pass 2019-06-21 05:41:49 +01:00
Zlatin Balevsky
b23226e8c6 wip on parsing X-Have from uploader 2019-06-21 05:30:56 +01:00
Zlatin Balevsky
1249ad29e0 claim pieces from list of available pieces 2019-06-21 04:42:02 +01:00
Zlatin Balevsky
7bb5e5b632 Release 0.3.4 2019-06-20 21:07:50 +01:00
Zlatin Balevsky
b2e43f9765 update split pattern and add unit test 2019-06-20 21:06:39 +01:00
Zlatin Balevsky
2aa73c203a Release 0.3.3 2019-06-20 18:08:02 +01:00
Zlatin Balevsky
18d2b56563 fix indexing 2019-06-20 17:57:36 +01:00
Zlatin Balevsky
a455b4ad6e redirect exceptions in result sender to log 2019-06-20 17:22:59 +01:00
Zlatin Balevsky
761b683a81 Release 0.3.2 2019-06-20 16:04:46 +01:00
Zlatin Balevsky
1d41bcd825 prevent empty tokens in search index 2019-06-20 16:02:48 +01:00
Zlatin Balevsky
f1ac038b55 update split pattern 2019-06-20 15:47:00 +01:00
Zlatin Balevsky
396c636e42 prevent empty search terms 2019-06-20 15:29:27 +01:00
Zlatin Balevsky
e32c858e90 update README with quick FAQ 2019-06-20 14:18:37 +01:00
Zlatin Balevsky
821555f3f1 Release 0.3.1 2019-06-20 14:02:22 +01:00
Zlatin Balevsky
089ab4f0d9 do not retry downloads if core is shut(ting) down 2019-06-20 13:40:04 +01:00
Zlatin Balevsky
948b6292fe add shutdown hook to shutdown core on SIGTERM 2019-06-20 13:29:15 +01:00
Zlatin Balevsky
4e2a530a13 Release 0.3.0 2019-06-20 07:04:45 +01:00
Zlatin Balevsky
03646e2b90 Document download mesh 2019-06-20 01:19:15 +01:00
Zlatin Balevsky
3dce228bbb always clean 2019-06-19 22:42:05 +01:00
Zlatin Balevsky
15a49ad550 show git revision in title 2019-06-19 22:36:22 +01:00
Zlatin Balevsky
3d91c0f4c7 increase default tunnel count 2019-06-19 22:24:04 +01:00
Zlatin Balevsky
2825a8d9a4 Release 0.2.10 2019-06-19 17:18:30 +01:00
Zlatin Balevsky
8dcce9bda6 Merge branch 'connection-logic' 2019-06-19 17:16:13 +01:00
Zlatin Balevsky
d8d3e2cd58 update tests 2019-06-19 15:54:35 +01:00
Zlatin Balevsky
51d5dbe47e Prevent rare exception on changing trust when result tabs are open 2019-06-19 12:23:18 +01:00
Zlatin Balevsky
84cee0aa43 retry failed hosts after one hour 2019-06-19 08:35:31 +01:00
Zlatin Balevsky
162844787f explicitly set java versions 2019-06-19 02:11:00 +01:00
Zlatin Balevsky
d8a2b59055 tool to print out contents of files.json 2019-06-18 22:08:33 +01:00
Zlatin Balevsky
67a0939de4 Release 0.2.9 2019-06-18 20:15:53 +01:00
Zlatin Balevsky
37ca922a2c reduce default retry interval 2019-06-18 20:07:20 +01:00
Zlatin Balevsky
1d6781819b ignore CWSE if shutting down 2019-06-18 19:44:22 +01:00
Zlatin Balevsky
64d45da94a show version on title 2019-06-18 18:57:44 +01:00
Zlatin Balevsky
59c84d8a5e Release 0.2.8 2019-06-18 17:48:07 +01:00
Zlatin Balevsky
8b55021a4b fix 2019-06-18 17:23:18 +01:00
Zlatin Balevsky
8bd3ebfaf5 timestamp entries 2019-06-18 17:17:03 +01:00
Zlatin Balevsky
526ec45da3 Release 0.2.7 2019-06-18 15:53:54 +01:00
Zlatin Balevsky
deb7c0b4b0 exclude files present locally from search results 2019-06-18 15:45:27 +01:00
Zlatin Balevsky
e85a0c7b2c Merge branch 'source-tracking' 2019-06-18 12:22:46 +01:00
Zlatin Balevsky
7b021a47eb fix detection of moving files into a watched dir on Linux 2019-06-18 12:20:10 +01:00
Zlatin Balevsky
0c21d4d6c1 implement source tracking 2019-06-18 11:34:19 +01:00
Zlatin Balevsky
8e9f79d404 update TODO 2019-06-18 09:43:22 +01:00
Zlatin Balevsky
bf33a6ff61 Release 0.2.6 2019-06-18 09:07:27 +01:00
Zlatin Balevsky
19c8d84afd Merge branch 'file-monitor' 2019-06-18 09:01:09 +01:00
Zlatin Balevsky
6a40787863 fine log 2019-06-18 05:46:16 +01:00
Zlatin Balevsky
c698cbd737 register created directories recursively 2019-06-18 05:43:41 +01:00
Zlatin Balevsky
9c049b9301 special case mac 2019-06-18 05:26:41 +01:00
Zlatin Balevsky
84a9bb9482 watch deleting of files 2019-06-18 04:15:44 +01:00
Zlatin Balevsky
0c1008d6b3 update readme 2019-06-18 04:01:04 +01:00
Zlatin Balevsky
c46f1b1ccd delay processing of files until after 1 second after the last MODIFY event 2019-06-17 23:08:16 +01:00
Zlatin Balevsky
7e2c4d48c6 wait for UI to load before loading files 2019-06-17 22:34:19 +01:00
Zlatin Balevsky
71a919e62b shut down watcher before connection manager 2019-06-17 22:15:50 +01:00
Zlatin Balevsky
d5eb65bdc2 do not print stacktrace on clean shutdown 2019-06-17 21:58:44 +01:00
Zlatin Balevsky
aef7533bd5 make watcher thread daemon 2019-06-17 19:58:57 +01:00
Zlatin Balevsky
e78016ead4 ui panel for managing watched directories 2019-06-17 19:23:04 +01:00
Zlatin Balevsky
52ced669dd basic watching of directories 2019-06-17 16:36:12 +01:00
Zlatin Balevsky
b52fb38ede fix disabling of buttons on search tab close 2019-06-17 13:43:11 +01:00
Zlatin Balevsky
5dcef3ca05 Release 0.2.5 2019-06-17 12:53:58 +01:00
Zlatin Balevsky
eaa0e46ce5 Merge branch 'separate-incomplete-files' 2019-06-17 12:45:51 +01:00
Zlatin Balevsky
5c16335969 if no row is selected do not enable buttons 2019-06-17 12:26:28 +01:00
Zlatin Balevsky
546eb4e9d3 only allow one download per infohash from gui 2019-06-17 11:25:21 +01:00
59 changed files with 1510 additions and 238 deletions

View File

@@ -4,26 +4,26 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder. The current stable release - 0.2.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
### Building ### Building
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
``` ```
./gradlew assemble ./gradlew clean assemble
``` ```
If you want to run the unit tests, type If you want to run the unit tests, type
``` ```
./gradlew build ./gradlew clean build
``` ```
Some of the UI tests will fail because they haven't been written yet :-/ Some of the UI tests will fail because they haven't been written yet :-/
### Running ### Running
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.post=<port>" in there. You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.port=<port>" in there.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P. The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
@@ -31,3 +31,26 @@ The first time you run MuWire it will ask you to select a nickname. This nickna
### Known bugs and limitations ### Known bugs and limitations
* Many UI features you would expect are not there yet * Many UI features you would expect are not there yet
### Quick FAQ
* why is MuWire slow ?
- too few sources you're downloading from
- you can increase the number of tunnels by using more tunnels via Options->I2P Inbound/Outbound Quantity
the default is 4 and you could raise up to as high as 16 ( Caution !!!!)
* my search is not returning (enough) results !
- search is keyword or hash based
- keywords and hash(es) are NOT regexed or wildcarded so they have to be complete
so searching for 'musi' will not return results with 'music' - you have to search for 'music'
- ALL keywords have to match
- only use space for keyword separation
- if you already have the file in question it is not displayed ( can be changed via Options )
* what's this right click -> 'Copy hash to clipboard' for ?
- if you have a specific file you wish to share or download you can use the hash as a unique identifier
to make sure you have exactly the right file.
- you can share this hash with others to ensure they are getting the right file

11
TODO.md
View File

@@ -4,10 +4,6 @@ Not in any particular order yet
### Big Items ### Big Items
##### Alternate Locations
This helps peers discover new sources for a file while the download is in progress. Also makes sharing of partial files possible.
##### Bloom Filters ##### Bloom Filters
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
@@ -32,11 +28,14 @@ For ease of deployment for new users, and so that users do not need to run a sep
Basically any non-gui non-cli user interface Basically any non-gui non-cli user interface
##### Metadata editing and search
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items ### Small Items
* Detect if router is dead and show warning or exit
* Wrapper of some kind for in-place upgrades * Wrapper of some kind for in-place upgrades
* Download file sequentially * Download file sequentially
* Unsharing of files * Unsharing of files
* Multiple-selection download, Ctrl-A * Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item) * Pause/Resume downloads

View File

@@ -4,6 +4,7 @@ import java.util.concurrent.CountDownLatch
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.connection.DisconnectionEvent
@@ -34,7 +35,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.2.4") core = new Core(props, home, "0.3.6")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"
@@ -72,7 +73,7 @@ class Cli {
Timer timer = new Timer("status-printer", true) Timer timer = new Timer("status-printer", true)
timer.schedule({ timer.schedule({
println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared" println String.valueOf(new Date()) + " Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
} as TimerTask, 60000, 60000) } as TimerTask, 60000, 60000)
def latch = new CountDownLatch(1) def latch = new CountDownLatch(1)
@@ -84,6 +85,7 @@ class Cli {
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader) core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
core.startServices() core.startServices()
core.eventBus.publish(new UILoadedEvent())
println "waiting for files to load" println "waiting for files to load"
latch.await() latch.await()
// now we begin // now we begin
@@ -117,11 +119,11 @@ class Cli {
volatile int uploads volatile int uploads
public void onUploadEvent(UploadEvent e) { public void onUploadEvent(UploadEvent e) {
uploads++ uploads++
println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}" println String.valueOf(new Date()) + " Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
} }
public void onUploadFinishedEvent(UploadFinishedEvent e) { public void onUploadFinishedEvent(UploadFinishedEvent e) {
uploads-- uploads--
println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}" println String.valueOf(new Date()) + " Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
} }
} }

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core Core core
try { try {
core = new Core(props, home, "0.2.4") core = new Core(props, home, "0.3.6")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

@@ -0,0 +1,23 @@
package com.muwire.cli
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import net.i2p.data.Base64
class FileList {
public static void main(String [] args) {
if (args.length < 1) {
println "pass files.json as argument"
System.exit(1)
}
def slurper = new JsonSlurper()
File filesJson = new File(args[0])
filesJson.eachLine {
def json = slurper.parseText(it)
String name = DataUtil.readi18nString(Base64.decode(json.file))
println "$name,$json.length,$json.pieceSize,$json.infoHash"
}
}
}

View File

@@ -11,5 +11,5 @@ class Constants {
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]" public static final String SPLIT_PATTERN = "[\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|]"
} }

View File

@@ -1,6 +1,7 @@
package com.muwire.core package com.muwire.core
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.connection.ConnectionAcceptor import com.muwire.core.connection.ConnectionAcceptor
import com.muwire.core.connection.ConnectionEstablisher import com.muwire.core.connection.ConnectionEstablisher
@@ -12,8 +13,11 @@ import com.muwire.core.connection.I2PConnector
import com.muwire.core.connection.LeafConnectionManager import com.muwire.core.connection.LeafConnectionManager
import com.muwire.core.connection.UltrapeerConnectionManager import com.muwire.core.connection.UltrapeerConnectionManager
import com.muwire.core.download.DownloadManager import com.muwire.core.download.DownloadManager
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
@@ -23,9 +27,11 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService import com.muwire.core.files.PersisterService
import com.muwire.core.files.DirectoryWatcher
import com.muwire.core.hostcache.CacheClient import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.mesh.MeshManager
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResultsEvent import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender import com.muwire.core.search.ResultsSender
@@ -44,6 +50,7 @@ import net.i2p.client.I2PSession
import net.i2p.client.streaming.I2PSocketManager import net.i2p.client.streaming.I2PSocketManager
import net.i2p.client.streaming.I2PSocketManagerFactory import net.i2p.client.streaming.I2PSocketManagerFactory
import net.i2p.client.streaming.I2PSocketOptions import net.i2p.client.streaming.I2PSocketOptions
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
import net.i2p.crypto.DSAEngine import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType import net.i2p.crypto.SigType
import net.i2p.data.Destination import net.i2p.data.Destination
@@ -70,6 +77,10 @@ public class Core {
private final ConnectionEstablisher connectionEstablisher private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService private final HasherService hasherService
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final DirectoryWatcher directoryWatcher
final FileManager fileManager
final AtomicBoolean shutdown = new AtomicBoolean()
public Core(MuWireSettings props, File home, String myVersion) { public Core(MuWireSettings props, File home, String myVersion) {
this.home = home this.home = home
@@ -101,9 +112,9 @@ public class Core {
i2pOptions["inbound.nickname"] = "MuWire" i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire" i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3" i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "2" i2pOptions["inbound.quantity"] = "4"
i2pOptions["outbound.length"] = "3" i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "2" i2pOptions["outbound.quantity"] = "4"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1" i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654" i2pOptions["i2cp.tcp.port"] = "7654"
} }
@@ -116,6 +127,7 @@ public class Core {
} }
socketManager.getDefaultOptions().setReadTimeout(60000) socketManager.getDefaultOptions().setReadTimeout(60000)
socketManager.getDefaultOptions().setConnectTimeout(30000) socketManager.getDefaultOptions().setConnectTimeout(30000)
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
i2pSession = socketManager.getSession() i2pSession = socketManager.getSession()
def destination = new Destination() def destination = new Destination()
@@ -153,15 +165,20 @@ public class Core {
log.info "initializing file manager" log.info "initializing file manager"
FileManager fileManager = new FileManager(eventBus, props) fileManager = new FileManager(eventBus, props)
eventBus.register(FileHashedEvent.class, fileManager) eventBus.register(FileHashedEvent.class, fileManager)
eventBus.register(FileLoadedEvent.class, fileManager) eventBus.register(FileLoadedEvent.class, fileManager)
eventBus.register(FileDownloadedEvent.class, fileManager) eventBus.register(FileDownloadedEvent.class, fileManager)
eventBus.register(FileUnsharedEvent.class, fileManager) eventBus.register(FileUnsharedEvent.class, fileManager)
eventBus.register(SearchEvent.class, fileManager) eventBus.register(SearchEvent.class, fileManager)
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager)
eventBus.register(SourceDiscoveredEvent.class, meshManager)
log.info "initializing persistence service" log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager) persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
log.info("initializing host cache") log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json") File hostStorage = new File(home, "hosts.json")
@@ -196,14 +213,17 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager) eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager") log.info("initializing download manager")
downloadManager = new DownloadManager(eventBus, i2pConnector, home, me) downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
eventBus.register(UIDownloadEvent.class, downloadManager) eventBus.register(UIDownloadEvent.class, downloadManager)
eventBus.register(UILoadedEvent.class, downloadManager) eventBus.register(UILoadedEvent.class, downloadManager)
eventBus.register(FileDownloadedEvent.class, downloadManager) eventBus.register(FileDownloadedEvent.class, downloadManager)
eventBus.register(UIDownloadCancelledEvent.class, downloadManager) eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
log.info("initializing upload manager") log.info("initializing upload manager")
UploadManager uploadManager = new UploadManager(eventBus, fileManager) UploadManager uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager)
log.info("initializing connection establisher") log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache) connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
@@ -213,6 +233,9 @@ public class Core {
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
eventBus.register(FileSharedEvent.class, directoryWatcher)
log.info("initializing hasher service") log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager) hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
@@ -221,9 +244,9 @@ public class Core {
public void startServices() { public void startServices() {
hasherService.start() hasherService.start()
directoryWatcher.start()
trustService.start() trustService.start()
trustService.waitForLoad() trustService.waitForLoad()
persisterService.start()
hostCache.start() hostCache.start()
connectionManager.start() connectionManager.start()
cacheClient.start() cacheClient.start()
@@ -234,12 +257,18 @@ public class Core {
} }
public void shutdown() { public void shutdown() {
if (!shutdown.compareAndSet(false, true)) {
log.info("already shutting down")
return
}
log.info("shutting down download manageer") log.info("shutting down download manageer")
downloadManager.shutdown() downloadManager.shutdown()
log.info("shutting down connection acceeptor") log.info("shutting down connection acceeptor")
connectionAcceptor.stop() connectionAcceptor.stop()
log.info("shutting down connection establisher") log.info("shutting down connection establisher")
connectionEstablisher.stop() connectionEstablisher.stop()
log.info("shutting down directory watcher")
directoryWatcher.stop()
log.info("shutting down connection manager") log.info("shutting down connection manager")
connectionManager.shutdown() connectionManager.shutdown()
} }
@@ -268,7 +297,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.2.4") Core core = new Core(props, home, "0.3.6")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -1,6 +1,11 @@
package com.muwire.core package com.muwire.core
import java.util.stream.Collectors
import com.muwire.core.hostcache.CrawlerResponse import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class MuWireSettings { class MuWireSettings {
@@ -10,10 +15,9 @@ class MuWireSettings {
int updateCheckInterval int updateCheckInterval
String nickname String nickname
File downloadLocation File downloadLocation
String sharedFiles
CrawlerResponse crawlerResponse CrawlerResponse crawlerResponse
boolean shareDownloadedFiles boolean shareDownloadedFiles
boolean watchSharedDirectories Set<String> watchedDirectories
MuWireSettings() { MuWireSettings() {
this(new Properties()) this(new Properties())
@@ -26,11 +30,16 @@ class MuWireSettings {
nickname = props.getProperty("nickname","MuWireUser") nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles") downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15")) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true"))
watchedDirectories = new HashSet<>()
if (props.containsKey("watchedDirectories")) {
String[] encoded = props.getProperty("watchedDirectories").split(",")
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
}
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -43,9 +52,14 @@ class MuWireSettings {
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval)) props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval)) props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
if (sharedFiles != null) if (!watchedDirectories.isEmpty()) {
props.setProperty("sharedFiles", sharedFiles) String encoded = watchedDirectories.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty("watchedDirectories", encoded)
}
props.store(out, "") props.store(out, "")
} }

View File

@@ -0,0 +1,4 @@
package com.muwire.core
class RouterDisconnectedEvent extends Event {
}

View File

@@ -3,6 +3,10 @@ package com.muwire.core.download
import com.muwire.core.connection.I2PConnector import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import groovy.json.JsonBuilder import groovy.json.JsonBuilder
@@ -14,24 +18,33 @@ import net.i2p.util.ConcurrentHashSet
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent import com.muwire.core.UILoadedEvent
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
public class DownloadManager { public class DownloadManager {
private final EventBus eventBus private final EventBus eventBus
private final TrustService trustService
private final MeshManager meshManager
private final MuWireSettings muSettings
private final I2PConnector connector private final I2PConnector connector
private final Executor executor private final Executor executor
private final File incompletes, home private final File incompletes, home
private final Persona me private final Persona me
private final Set<Downloader> downloaders = new ConcurrentHashSet<>() private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) { public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
I2PConnector connector, File home, Persona me) {
this.eventBus = eventBus this.eventBus = eventBus
this.trustService = trustService
this.meshManager = meshManager
this.muSettings = muSettings
this.connector = connector this.connector = connector
this.incompletes = new File(home,"incompletes") this.incompletes = new File(home,"incompletes")
this.home = home this.home = home
@@ -58,18 +71,30 @@ public class DownloadManager {
e.result.each { e.result.each {
destinations.add(it.sender.destination) destinations.add(it.sender.destination)
} }
destinations.addAll(e.sources)
destinations.remove(me.destination)
Pieces pieces = getPieces(infohash, size, pieceSize)
def downloader = new Downloader(eventBus, this, me, e.target, size, def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations, infohash, pieceSize, connector, destinations,
incompletes) incompletes, pieces)
downloaders.add(downloader) downloaders.put(infohash, downloader)
persistDownloaders() persistDownloaders()
executor.execute({downloader.download()} as Runnable) executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader)) eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} }
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) { public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
downloaders.remove(e.downloader) downloaders.remove(e.downloader.infoHash)
persistDownloaders()
}
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
persistDownloaders()
}
public void onUIDownloadResumedEvent(UIDownloadResumedEvent e) {
persistDownloaders() persistDownloaders()
} }
@@ -97,23 +122,54 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot) byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root) infoHash = new InfoHash(root)
} }
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length, def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes) infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
downloaders.add(downloader) if (json.paused != null)
downloader.download() downloader.paused = json.paused
downloaders.put(infoHash, downloader)
downloader.readPieces()
if (!downloader.paused)
downloader.download()
eventBus.publish(new DownloadStartedEvent(downloader : downloader)) eventBus.publish(new DownloadStartedEvent(downloader : downloader))
} }
} }
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) {
int pieceSize = 0x1 << pieceSizePow2
int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0)
nPieces++
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces)
mesh.pieces
}
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
Downloader downloader = downloaders.get(e.infoHash)
if (downloader == null)
return
boolean ok = false
switch(trustService.getLevel(e.source.destination)) {
case TrustLevel.TRUSTED: ok = true; break
case TrustLevel.NEUTRAL: ok = muSettings.allowUntrusted; break
case TrustLevel.DISTRUSTED: ok = false; break
}
if (ok)
downloader.addSource(e.source.destination)
}
void onFileDownloadedEvent(FileDownloadedEvent e) { void onFileDownloadedEvent(FileDownloadedEvent e) {
downloaders.remove(e.downloader) downloaders.remove(e.downloader.infoHash)
persistDownloaders() persistDownloaders()
} }
private void persistDownloaders() { private void persistDownloaders() {
File downloadsFile = new File(home,"downloads.json") File downloadsFile = new File(home,"downloads.json")
downloadsFile.withPrintWriter { writer -> downloadsFile.withPrintWriter { writer ->
downloaders.each { downloader -> downloaders.values().each { downloader ->
if (!downloader.cancelled) { if (!downloader.cancelled) {
def json = [:] def json = [:]
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath())) json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
@@ -130,6 +186,8 @@ public class DownloadManager {
json.hashList = Base64.encode(infoHash.hashList) json.hashList = Base64.encode(infoHash.hashList)
else else
json.hashRoot = Base64.encode(infoHash.getRoot()) json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused
writer.println(JsonOutput.toJson(json)) writer.println(JsonOutput.toJson(json))
} }
} }
@@ -137,7 +195,7 @@ public class DownloadManager {
} }
public void shutdown() { public void shutdown() {
downloaders.each { it.stop() } downloaders.values().each { it.stop() }
Downloader.executorService.shutdownNow() Downloader.executorService.shutdownNow()
} }
} }

View File

@@ -3,8 +3,12 @@ package com.muwire.core.download;
import net.i2p.data.Base64 import net.i2p.data.Base64
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.util.DataUtil
import static com.muwire.core.util.DataUtil.readTillRN import static com.muwire.core.util.DataUtil.readTillRN
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -23,6 +27,7 @@ class DownloadSession {
private static int SAMPLES = 10 private static int SAMPLES = 10
private final EventBus eventBus
private final String meB64 private final String meB64
private final Pieces pieces private final Pieces pieces
private final InfoHash infoHash private final InfoHash infoHash
@@ -30,6 +35,7 @@ class DownloadSession {
private final File file private final File file
private final int pieceSize private final int pieceSize
private final long fileLength private final long fileLength
private final Set<Integer> available
private final MessageDigest digest private final MessageDigest digest
private final LinkedList<Long> timestamps = new LinkedList<>() private final LinkedList<Long> timestamps = new LinkedList<>()
@@ -37,8 +43,9 @@ class DownloadSession {
private ByteBuffer mapped private ByteBuffer mapped
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file, DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) { int pieceSize, long fileLength, Set<Integer> available) {
this.eventBus = eventBus
this.meB64 = meB64 this.meB64 = meB64
this.pieces = pieces this.pieces = pieces
this.endpoint = endpoint this.endpoint = endpoint
@@ -46,6 +53,7 @@ class DownloadSession {
this.file = file this.file = file
this.pieceSize = pieceSize this.pieceSize = pieceSize
this.fileLength = fileLength this.fileLength = fileLength
this.available = available
try { try {
digest = MessageDigest.getInstance("SHA-256") digest = MessageDigest.getInstance("SHA-256")
} catch (NoSuchAlgorithmException impossible) { } catch (NoSuchAlgorithmException impossible) {
@@ -63,7 +71,11 @@ class DownloadSession {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream() InputStream is = endpoint.getInputStream()
int piece = pieces.claim() int piece
if (available.isEmpty())
piece = pieces.claim()
else
piece = pieces.claim(available)
if (piece == -1) if (piece == -1)
return false return false
boolean unclaim = true boolean unclaim = true
@@ -79,45 +91,79 @@ class DownloadSession {
try { try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII))
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() os.flush()
String code = readTillRN(is) String codeString = readTillRN(is)
if (code.startsWith("404 ")) { int space = codeString.indexOf(' ')
if (space > 0)
codeString = codeString.substring(0, space)
int code = Integer.parseInt(codeString.trim())
if (code == 404) {
log.warning("file not found") log.warning("file not found")
endpoint.close() endpoint.close()
return return false
} }
if (code.startsWith("416 ")) { if (!(code == 200 || code == 416)) {
log.warning("range $start-$end cannot be satisfied")
return // leave endpoint open
}
if (!code.startsWith("200 ")) {
log.warning("unknown code $code") log.warning("unknown code $code")
endpoint.close() endpoint.close()
return false return false
} }
// parse all headers // parse all headers
Set<String> headers = new HashSet<>() Map<String,String> headers = new HashMap<>()
String header String header
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS) {
headers.add(header) int colon = header.indexOf(':')
if (colon == -1 || colon == header.length() - 1)
long receivedStart = -1 throw new IOException("invalid header $header")
long receivedEnd = -1 String key = header.substring(0, colon)
for (String receivedHeader : headers) { String value = header.substring(colon + 1)
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/) headers[key] = value.trim()
if (group.size() != 1) {
log.info("ignoring header $receivedHeader")
continue
}
receivedStart = Long.parseLong(group[0][1])
receivedEnd = Long.parseLong(group[0][2])
} }
// prase X-Alt if present
if (headers.containsKey("X-Alt")) {
headers["X-Alt"].split(",").each {
if (it.length() > 0) {
byte [] raw = Base64.decode(it)
Persona source = new Persona(new ByteArrayInputStream(raw))
eventBus.publish(new SourceDiscoveredEvent(infoHash : infoHash, source : source))
}
}
}
// parse X-Have if present
if (headers.containsKey("X-Have")) {
DataUtil.decodeXHave(headers["X-Have"]).each {
available.add(it)
}
if (!available.contains(piece))
return true // try again next time
} else {
if (code != 200)
throw new IOException("Code $code but no X-Have")
available.clear()
}
if (code != 200)
return true
String range = headers["Content-Range"]
if (range == null)
throw new IOException("Code 200 but no Content-Range")
def group = (range =~ /^(\d+)-(\d+)$/)
if (group.size() != 1)
throw new IOException("invalid Content-Range header $range")
long receivedStart = Long.parseLong(group[0][1])
long receivedEnd = Long.parseLong(group[0][2])
if (receivedStart != start || receivedEnd != end) { if (receivedStart != start || receivedEnd != end) {
log.warning("We don't support mismatching ranges yet") log.warning("We don't support mismatching ranges yet")
endpoint.close() endpoint.close()

View File

@@ -21,10 +21,11 @@ import com.muwire.core.files.FileDownloadedEvent
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@Log @Log
public class Downloader { public class Downloader {
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED } public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED} private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r -> private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
@@ -49,16 +50,17 @@ public class Downloader {
private final File incompleteFile private final File incompleteFile
final int pieceSizePow2 final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>() private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
private volatile boolean cancelled private volatile boolean cancelled, paused
private final AtomicBoolean eventFired = new AtomicBoolean() private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed private boolean piecesFileClosed
public Downloader(EventBus eventBus, DownloadManager downloadManager, public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash, Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations, int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) { File incompletes, Pieces pieces) {
this.eventBus = eventBus this.eventBus = eventBus
this.me = me this.me = me
this.downloadManager = downloadManager this.downloadManager = downloadManager
@@ -71,15 +73,8 @@ public class Downloader {
this.incompleteFile = new File(incompletes, file.getName()+".part") this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2 this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces
int nPieces this.nPieces = pieces.nPieces
if (length % pieceSize == 0)
nPieces = length / pieceSize
else
nPieces = length / pieceSize + 1
this.nPieces = nPieces
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
} }
public synchronized InfoHash getInfoHash() { public synchronized InfoHash getInfoHash() {
@@ -141,6 +136,9 @@ public class Downloader {
public DownloadState getCurrentState() { public DownloadState getCurrentState() {
if (cancelled) if (cancelled)
return DownloadState.CANCELLED return DownloadState.CANCELLED
if (paused)
return DownloadState.PAUSED
boolean allFinished = true boolean allFinished = true
activeWorkers.values().each { activeWorkers.values().each {
allFinished &= it.currentState == WorkerState.FINISHED allFinished &= it.currentState == WorkerState.FINISHED
@@ -185,6 +183,12 @@ public class Downloader {
piecesFile.delete() piecesFile.delete()
} }
incompleteFile.delete() incompleteFile.delete()
pieces.clearAll()
}
public void pause() {
paused = true
stop()
} }
void stop() { void stop() {
@@ -203,6 +207,8 @@ public class Downloader {
} }
public void resume() { public void resume() {
paused = false
readPieces()
destinations.each { destination -> destinations.each { destination ->
def worker = activeWorkers.get(destination) def worker = activeWorkers.get(destination)
if (worker != null) { if (worker != null) {
@@ -219,12 +225,21 @@ public class Downloader {
} }
} }
void addSource(Destination d) {
if (activeWorkers.containsKey(d))
return
DownloadWorker newWorker = new DownloadWorker(d)
activeWorkers.put(d, newWorker)
executorService.submit(newWorker)
}
class DownloadWorker implements Runnable { class DownloadWorker implements Runnable {
private final Destination destination private final Destination destination
private volatile WorkerState currentState private volatile WorkerState currentState
private volatile Thread downloadThread private volatile Thread downloadThread
private Endpoint endpoint private Endpoint endpoint
private volatile DownloadSession currentSession private volatile DownloadSession currentSession
private final Set<Integer> available = new HashSet<>()
DownloadWorker(Destination destination) { DownloadWorker(Destination destination) {
this.destination = destination this.destination = destination
@@ -245,10 +260,12 @@ public class Downloader {
currentState = WorkerState.DOWNLOADING currentState = WorkerState.DOWNLOADING
boolean requestPerformed boolean requestPerformed
while(!pieces.isComplete()) { while(!pieces.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, incompleteFile, pieceSize, length) currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available)
requestPerformed = currentSession.request() requestPerformed = currentSession.request()
if (!requestPerformed) if (!requestPerformed)
break break
successfulDestinations.add(endpoint.destination)
writePieces() writePieces()
} }
} catch (Exception bad) { } catch (Exception bad) {
@@ -268,7 +285,7 @@ public class Downloader {
} }
eventBus.publish( eventBus.publish(
new FileDownloadedEvent( new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()), downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this)) downloader : Downloader.this))
} }

View File

@@ -38,6 +38,18 @@ class Pieces {
} }
} }
synchronized int claim(Set<Integer> available) {
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
available.remove(i)
if (available.isEmpty())
return -1
List<Integer> toList = available.toList()
Collections.shuffle(toList)
int rv = toList[0]
claimed.set(rv)
rv
}
synchronized def getDownloaded() { synchronized def getDownloaded() {
def rv = [] def rv = []
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) { for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
@@ -62,4 +74,13 @@ class Pieces {
synchronized int donePieces() { synchronized int donePieces() {
done.cardinality() done.cardinality()
} }
synchronized boolean isDownloaded(int piece) {
done.get(piece)
}
synchronized void clearAll() {
done.clear()
claimed.clear()
}
} }

View File

@@ -0,0 +1,10 @@
package com.muwire.core.download
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.Persona
class SourceDiscoveredEvent extends Event {
InfoHash infoHash
Persona source
}

View File

@@ -3,8 +3,11 @@ package com.muwire.core.download
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import net.i2p.data.Destination
class UIDownloadEvent extends Event { class UIDownloadEvent extends Event {
UIResultEvent[] result UIResultEvent[] result
Set<Destination> sources
File target File target
} }

View File

@@ -0,0 +1,6 @@
package com.muwire.core.download
import com.muwire.core.Event
class UIDownloadPausedEvent extends Event {
}

View File

@@ -0,0 +1,6 @@
package com.muwire.core.download
import com.muwire.core.Event
class UIDownloadResumedEvent extends Event {
}

View File

@@ -1,4 +1,141 @@
package com.muwire.core.files package com.muwire.core.files
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths
import static java.nio.file.StandardWatchEventKinds.*
import java.nio.file.ClosedWatchServiceException
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.nio.file.WatchService
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.SharedFile
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
@Log
class DirectoryWatcher { class DirectoryWatcher {
private static final long WAIT_TIME = 1000
private static final WatchEvent.Kind[] kinds
static {
if (SystemVersion.isMac())
kinds = [ENTRY_MODIFY, ENTRY_DELETE]
else
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
}
private final EventBus eventBus
private final FileManager fileManager
private final Thread watcherThread, publisherThread
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
this.eventBus = eventBus
this.fileManager = fileManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
watcherThread.setDaemon(true)
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
publisherThread.setDaemon(true)
}
void start() {
watchService = FileSystems.getDefault().newWatchService()
watcherThread.start()
publisherThread.start()
}
void stop() {
shutdown = true
watcherThread.interrupt()
publisherThread.interrupt()
watchService.close()
}
void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory())
return
Path path = e.file.getCanonicalFile().toPath()
path.register(watchService, kinds)
}
private void watch() {
try {
while(!shutdown) {
WatchKey key = watchService.take()
key.pollEvents().each {
switch(it.kind()) {
case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break
case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break
case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break
}
}
key.reset()
}
} catch (InterruptedException|ClosedWatchServiceException e) {
if (!shutdown)
throw e
}
}
private void processCreated(Path parent, Path path) {
File f= join(parent, path)
log.fine("created entry $f")
if (f.isDirectory())
f.toPath().register(watchService, kinds)
else
waitingFiles.put(f, System.currentTimeMillis())
}
private void processModified(Path parent, Path path) {
File f = join(parent, path)
log.fine("modified entry $f")
waitingFiles.put(f, System.currentTimeMillis())
}
private void processDeleted(Path parent, Path path) {
File f = join(parent, path)
log.fine("deleted entry $f")
SharedFile sf = fileManager.fileToSharedFile.get(f)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
}
private static File join(Path parent, Path path) {
File parentFile = parent.toFile().getCanonicalFile()
new File(parentFile, path.toFile().getName())
}
private void publish() {
try {
while(!shutdown) {
Thread.sleep(WAIT_TIME)
long now = System.currentTimeMillis()
def published = []
waitingFiles.each { file, timestamp ->
if (now - timestamp > WAIT_TIME) {
log.fine("publishing file $file")
eventBus.publish new FileSharedEvent(file : file)
published << file
}
}
published.each {
waitingFiles.remove(it)
}
}
} catch (InterruptedException e) {
if (!shutdown)
throw e
}
}
} }

View File

@@ -32,7 +32,7 @@ class HasherService {
private void process(File f) { private void process(File f) {
f = f.getCanonicalFile() f = f.getCanonicalFile()
if (f.isDirectory()) { if (f.isDirectory()) {
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) } f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
} else { } else {
if (f.length() == 0) { if (f.length() == 0) {
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f") eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")

View File

@@ -11,6 +11,7 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Service import com.muwire.core.Service
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput import groovy.json.JsonOutput
@@ -36,14 +37,14 @@ class PersisterService extends Service {
timer = new Timer("file persister", true) timer = new Timer("file persister", true)
} }
void start() {
timer.schedule({load()} as TimerTask, 1)
}
void stop() { void stop() {
timer.cancel() timer.cancel()
} }
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void load() { void load() {
if (location.exists() && location.isFile()) { if (location.exists() && location.isFile()) {
def slurper = new JsonSlurper() def slurper = new JsonSlurper()

View File

@@ -5,9 +5,11 @@ import net.i2p.data.Destination
class Host { class Host {
private static final int MAX_FAILURES = 3 private static final int MAX_FAILURES = 3
private static final int CLEAR_INTERVAL = 60 * 60 * 1000
final Destination destination final Destination destination
int failures,successes int failures,successes
long lastAttempt
public Host(Destination destination) { public Host(Destination destination) {
this.destination = destination this.destination = destination
@@ -16,11 +18,13 @@ class Host {
synchronized void onConnect() { synchronized void onConnect() {
failures = 0 failures = 0
successes++ successes++
lastAttempt = System.currentTimeMillis()
} }
synchronized void onFailure() { synchronized void onFailure() {
failures++ failures++
successes = 0 successes = 0
lastAttempt = System.currentTimeMillis()
} }
synchronized boolean isFailed() { synchronized boolean isFailed() {
@@ -34,4 +38,8 @@ class Host {
synchronized void clearFailures() { synchronized void clearFailures() {
failures = 0 failures = 0
} }
synchronized void canTryAgain() {
System.currentTimeMillis() - lastAttempt > CLEAR_INTERVAL
}
} }

View File

@@ -109,6 +109,8 @@ class HostCache extends Service {
Host host = new Host(dest) Host host = new Host(dest)
host.failures = Integer.valueOf(String.valueOf(entry.failures)) host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes)) host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null)
host.lastAttempt = entry.lastAttempt
if (allowHost(host)) if (allowHost(host))
hosts.put(dest, host) hosts.put(dest, host)
} }
@@ -118,7 +120,7 @@ class HostCache extends Service {
} }
private boolean allowHost(Host host) { private boolean allowHost(Host host) {
if (host.isFailed()) if (host.isFailed() && !host.canTryAgain())
return false return false
if (host.destination == myself) if (host.destination == myself)
return false return false
@@ -143,6 +145,7 @@ class HostCache extends Service {
map.destination = dest.toBase64() map.destination = dest.toBase64()
map.failures = host.failures map.failures = host.failures
map.successes = host.successes map.successes = host.successes
map.lastAttempt = host.lastAttempt
def json = JsonOutput.toJson(map) def json = JsonOutput.toJson(map)
writer.println json writer.println json
} }

View File

@@ -0,0 +1,28 @@
package com.muwire.core.mesh
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
class Mesh {
private final InfoHash infoHash
private final Set<Persona> sources = new ConcurrentHashSet<>()
private final Pieces pieces
Mesh(InfoHash infoHash, Pieces pieces) {
this.infoHash = infoHash
this.pieces = pieces
}
Set<Persona> getRandom(int n, Persona exclude) {
List<Persona> tmp = new ArrayList<>(sources)
tmp.remove(exclude)
Collections.shuffle(tmp)
if (tmp.size() < n)
return tmp
tmp[0..n-1]
}
}

View File

@@ -0,0 +1,43 @@
package com.muwire.core.mesh
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.download.Pieces
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager
class MeshManager {
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
private final FileManager fileManager
MeshManager(FileManager fileManager) {
this.fileManager = fileManager
}
Mesh get(InfoHash infoHash) {
meshes.get(infoHash)
}
Mesh getOrCreate(InfoHash infoHash, int nPieces) {
synchronized(meshes) {
if (meshes.containsKey(infoHash))
return meshes.get(infoHash)
Pieces pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
if (fileManager.rootToFiles.containsKey(infoHash)) {
for (int i = 0; i < nPieces; i++)
pieces.markDownloaded(i)
}
Mesh rv = new Mesh(infoHash, pieces)
meshes.put(infoHash, rv)
return rv
}
}
void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
Mesh mesh = meshes.get(e.infoHash)
if (mesh == null)
return
mesh.sources.add(e.source)
}
}

View File

@@ -1,5 +1,7 @@
package com.muwire.core.search package com.muwire.core.search
import java.util.stream.Collectors
import javax.naming.directory.InvalidSearchControlsException import javax.naming.directory.InvalidSearchControlsException
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
@@ -7,6 +9,7 @@ import com.muwire.core.Persona
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Destination
class ResultsParser { class ResultsParser {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException { public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
@@ -58,6 +61,7 @@ class ResultsParser {
size : size, size : size,
infohash : parsedIH, infohash : parsedIH,
pieceSize : pieceSize, pieceSize : pieceSize,
sources : Collections.emptySet(),
uuid : uuid) uuid : uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)
@@ -82,11 +86,17 @@ class ResultsParser {
if (infoHash.length != InfoHash.SIZE) if (infoHash.length != InfoHash.SIZE)
throw new InvalidSearchResultException("invalid infohash size $infoHash.length") throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
int pieceSize = json.pieceSize int pieceSize = json.pieceSize
Set<Destination> sources = Collections.emptySet()
if (json.sources != null)
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
return new UIResultEvent( sender : p, return new UIResultEvent( sender : p,
name : name, name : name,
size : size, size : size,
infohash : new InfoHash(infoHash), infohash : new InfoHash(infoHash),
pieceSize : pieceSize, pieceSize : pieceSize,
sources : sources,
uuid: uuid) uuid: uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)

View File

@@ -11,7 +11,10 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
@@ -54,12 +57,16 @@ class ResultsSender {
int pieceSize = it.getPieceSize() int pieceSize = it.getPieceSize()
if (pieceSize == 0) if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length) pieceSize = FileHasher.getPieceSize(length)
Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile)
suggested = it.sources
def uiResultEvent = new UIResultEvent( sender : me, def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
infohash : it.getInfoHash(), infohash : it.getInfoHash(),
pieceSize : pieceSize, pieceSize : pieceSize,
uuid : uuid uuid : uuid,
sources : suggested
) )
eventBus.publish(uiResultEvent) eventBus.publish(uiResultEvent)
} }
@@ -77,46 +84,54 @@ class ResultsSender {
@Override @Override
public void run() { public void run() {
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null;
try { try {
endpoint = connector.connect(target) byte [] tmp = new byte[InfoHash.SIZE]
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream()) JsonOutput jsonOutput = new JsonOutput()
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) Endpoint endpoint = null;
me.write(os) try {
os.writeShort((short)results.length) endpoint = connector.connect(target)
results.each { DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8) os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
def baos = new ByteArrayOutputStream() me.write(os)
def daos = new DataOutputStream(baos) os.writeShort((short)results.length)
daos.writeShort((short) name.length) results.each {
daos.write(name) byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
daos.flush() def baos = new ByteArrayOutputStream()
String encodedName = Base64.encode(baos.toByteArray()) def daos = new DataOutputStream(baos)
def obj = [:] daos.writeShort((short) name.length)
obj.type = "Result" daos.write(name)
obj.version = oobInfohash ? 2 : 1 daos.flush()
obj.name = encodedName String encodedName = Base64.encode(baos.toByteArray())
obj.infohash = Base64.encode(it.getInfoHash().getRoot()) def obj = [:]
obj.size = it.getFile().length() obj.type = "Result"
obj.pieceSize = it.getPieceSize() obj.version = oobInfohash ? 2 : 1
if (!oobInfohash) { obj.name = encodedName
byte [] hashList = it.getInfoHash().getHashList() obj.infohash = Base64.encode(it.getInfoHash().getRoot())
def hashListB64 = [] obj.size = it.getFile().length()
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) { obj.pieceSize = it.getPieceSize()
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE) if (!oobInfohash) {
hashListB64 << Base64.encode(tmp) byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
}
obj.hashList = hashListB64
} }
obj.hashList = hashListB64
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
} }
def json = jsonOutput.toJson(obj) os.flush()
os.writeShort((short)json.length()) } finally {
os.write(json.getBytes(StandardCharsets.US_ASCII)) endpoint?.close()
} }
os.flush() } catch (Exception e) {
} finally { log.log(Level.WARNING, "problem sending results",e)
endpoint?.close()
} }
} }
} }

View File

@@ -33,7 +33,10 @@ class SearchIndex {
private static String[] split(String source) { private static String[] split(String source) {
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase() source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source.split(" ") String [] split = source.split(" ")
def rv = []
split.each { if (it.length() > 0) rv << it }
rv.toArray(new String[0])
} }
String[] search(List<String> terms) { String[] search(List<String> terms) {

View File

@@ -4,8 +4,11 @@ import com.muwire.core.Event
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import net.i2p.data.Destination
class UIResultEvent extends Event { class UIResultEvent extends Event {
Persona sender Persona sender
Set<Destination> sources
UUID uuid UUID uuid
String name String name
long size long size

View File

@@ -2,4 +2,5 @@ package com.muwire.core.upload
class ContentRequest extends Request { class ContentRequest extends Request {
Range range Range range
boolean have
} }

View File

@@ -5,32 +5,56 @@ import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardOpenOption import java.nio.file.StandardOpenOption
import java.util.stream.Collectors
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.mesh.Mesh
import com.muwire.core.util.DataUtil
import net.i2p.data.Destination
class ContentUploader extends Uploader { class ContentUploader extends Uploader {
private final File file private final File file
private final ContentRequest request private final ContentRequest request
private final Mesh mesh
private final int pieceSize
ContentUploader(File file, ContentRequest request, Endpoint endpoint) { ContentUploader(File file, ContentRequest request, Endpoint endpoint, Mesh mesh, int pieceSize) {
super(endpoint) super(endpoint)
this.file = file this.file = file
this.request = request this.request = request
this.mesh = mesh
this.pieceSize = pieceSize
} }
@Override @Override
void respond() { void respond() {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
Range range = request.getRange() Range range = request.getRange()
if (range.start >= file.length() || range.end >= file.length()) { boolean satisfiable = true
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) final long length = file.length()
if (range.start >= length || range.end >= length)
satisfiable = false
if (satisfiable) {
int startPiece = range.start / (0x1 << pieceSize)
int endPiece = range.end / (0x1 << pieceSize)
for (int i = startPiece; i <= endPiece; i++)
satisfiable &= mesh.pieces.isDownloaded(i)
}
if (!satisfiable) {
os.write("416 Range Not Satisfiable\r\n".getBytes(StandardCharsets.US_ASCII))
writeMesh(request.downloader)
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush() os.flush()
return return
} }
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) os.write("Content-Range: $range.start-$range.end\r\n".getBytes(StandardCharsets.US_ASCII))
writeMesh(request.downloader)
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
FileChannel channel FileChannel channel
try { try {
@@ -51,6 +75,17 @@ class ContentUploader extends Uploader {
} }
} }
private void writeMesh(Persona toExclude) {
String xHave = DataUtil.encodeXHave(mesh.pieces.getDownloaded(), mesh.pieces.nPieces)
endpoint.getOutputStream().write("X-Have: $xHave\r\n".getBytes(StandardCharsets.US_ASCII))
Set<Persona> sources = mesh.getRandom(3, toExclude)
if (!sources.isEmpty()) {
String xAlts = sources.stream().map({ it.toBase64() }).collect(Collectors.joining(","))
endpoint.getOutputStream().write("X-Alt: $xAlts\r\n".getBytes(StandardCharsets.US_ASCII))
}
}
@Override @Override
public String getName() { public String getName() {
return file.getName(); return file.getName();

View File

@@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -48,8 +49,14 @@ class Request {
def decoded = Base64.decode(encoded) def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded)) downloader = new Persona(new ByteArrayInputStream(decoded))
} }
boolean have = false
if (headers.containsKey("X-Have")) {
def encoded = headers["X-Have"].trim()
have = DataUtil.decodeXHave(encoded).size() > 0
}
new ContentRequest( infoHash : infoHash, range : new Range(start, end), new ContentRequest( infoHash : infoHash, range : new Range(start, end),
headers : headers, downloader : downloader) headers : headers, downloader : downloader, have : have)
} }
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException { static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {

View File

@@ -6,7 +6,12 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Downloader
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager import com.muwire.core.files.FileManager
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -15,12 +20,17 @@ import net.i2p.data.Base64
public class UploadManager { public class UploadManager {
private final EventBus eventBus private final EventBus eventBus
private final FileManager fileManager private final FileManager fileManager
private final MeshManager meshManager
private final DownloadManager downloadManager
public UploadManager() {} public UploadManager() {}
public UploadManager(EventBus eventBus, FileManager fileManager) { public UploadManager(EventBus eventBus, FileManager fileManager,
MeshManager meshManager, DownloadManager downloadManager) {
this.eventBus = eventBus this.eventBus = eventBus
this.fileManager = fileManager this.fileManager = fileManager
this.meshManager = meshManager
this.downloadManager = downloadManager
} }
public void processGET(Endpoint e) throws IOException { public void processGET(Endpoint e) throws IOException {
@@ -44,8 +54,10 @@ public class UploadManager {
log.info("Responding to upload request for root $infoHashString") log.info("Responding to upload request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString) byte [] infoHashRoot = Base64.decode(infoHashString)
InfoHash infoHash = new InfoHash(infoHashRoot)
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot) Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (sharedFiles == null || sharedFiles.isEmpty()) { Downloader downloader = downloadManager.downloaders.get(infoHash)
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
log.info "file not found" log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush() e.getOutputStream().flush()
@@ -61,13 +73,31 @@ public class UploadManager {
return return
} }
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream()) ContentRequest request = Request.parseContentRequest(infoHash, e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) { if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination") log.info("Downloader persona doesn't match their destination")
e.close() e.close()
return return
} }
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
if (request.have)
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
Mesh mesh
File file
int pieceSize
if (downloader != null) {
mesh = meshManager.get(infoHash)
file = downloader.incompleteFile
pieceSize = downloader.pieceSizePow2
} else {
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
file = sharedFile.file
pieceSize = sharedFile.pieceSize
}
Uploader uploader = new ContentUploader(file, request, e, mesh, pieceSize)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {
uploader.respond() uploader.respond()
@@ -85,8 +115,10 @@ public class UploadManager {
log.info("Responding to hashlist request for root $infoHashString") log.info("Responding to hashlist request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString) byte [] infoHashRoot = Base64.decode(infoHashString)
InfoHash infoHash = new InfoHash(infoHashRoot)
Downloader downloader = downloadManager.downloaders.get(infoHash)
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot) Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (sharedFiles == null || sharedFiles.isEmpty()) { if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
log.info "file not found" log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush() e.getOutputStream().flush()
@@ -102,13 +134,30 @@ public class UploadManager {
return return
} }
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream()) Request request = Request.parseHashListRequest(infoHash, e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) { if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination") log.info("Downloader persona doesn't match their destination")
e.close() e.close()
return return
} }
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
InfoHash fullInfoHash
if (downloader == null) {
fullInfoHash = sharedFiles.iterator().next().infoHash
} else {
byte [] hashList = downloader.getInfoHash().getHashList()
if (hashList != null && hashList.length > 0)
fullInfoHash = downloader.getInfoHash()
else {
log.info("infohash not found in downloader")
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
e.close()
return
}
}
Uploader uploader = new HashListUploader(e, fullInfoHash, request)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {
uploader.respond() uploader.respond()
@@ -130,8 +179,10 @@ public class UploadManager {
log.info("Responding to upload request for root $infoHashString") log.info("Responding to upload request for root $infoHashString")
infoHashRoot = Base64.decode(infoHashString) infoHashRoot = Base64.decode(infoHashString)
infoHash = new InfoHash(infoHashRoot)
sharedFiles = fileManager.getSharedFiles(infoHashRoot) sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (sharedFiles == null || sharedFiles.isEmpty()) { downloader = downloadManager.downloaders.get(infoHash)
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
log.info "file not found" log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush() e.getOutputStream().flush()
@@ -153,7 +204,25 @@ public class UploadManager {
e.close() e.close()
return return
} }
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
if (request.have)
eventBus.publish(new SourceDiscoveredEvent(infoHash : request.infoHash, source : request.downloader))
Mesh mesh
File file
int pieceSize
if (downloader != null) {
mesh = meshManager.get(infoHash)
file = downloader.incompleteFile
pieceSize = downloader.pieceSizePow2
} else {
SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces)
file = sharedFile.file
pieceSize = sharedFile.pieceSize
}
uploader = new ContentUploader(file, request, e, mesh, pieceSize)
eventBus.publish(new UploadEvent(uploader : uploader)) eventBus.publish(new UploadEvent(uploader : uploader))
try { try {
uploader.respond() uploader.respond()

View File

@@ -4,6 +4,8 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants import com.muwire.core.Constants
import net.i2p.data.Base64
class DataUtil { class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1 private final static int MAX_SHORT = (0x1 << 16) - 1
@@ -79,4 +81,32 @@ class DataUtil {
} }
new String(baos.toByteArray(), StandardCharsets.US_ASCII) new String(baos.toByteArray(), StandardCharsets.US_ASCII)
} }
public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8
if (totalPieces % 8 != 0)
bytes++
byte[] raw = new byte[bytes]
pieces.each {
int byteIdx = it / 8
int offset = it % 8
int mask = 0x80 >>> offset
raw[byteIdx] |= mask
}
Base64.encode(raw)
}
public static List<Integer> decodeXHave(String xHave) {
byte [] availablePieces = Base64.decode(xHave)
List<Integer> available = new ArrayList<>()
availablePieces.eachWithIndex {b, i ->
for (int j = 0; j < 8 ; j++) {
byte mask = 0x80 >>> j
if ((b & mask) == mask) {
available.add(i * 8 + j)
}
}
}
available
}
} }

View File

@@ -26,6 +26,15 @@ public class SharedFile {
return pieceSize; return pieceSize;
} }
public int getNPieces() {
long length = file.length();
int rawPieceSize = 0x1 << pieceSize;
int rv = (int) (length / rawPieceSize);
if (length % pieceSize != 0)
rv++;
return rv;
}
@Override @Override
public int hashCode() { public int hashCode() {
return file.hashCode() ^ infoHash.hashCode(); return file.hashCode() ^ infoHash.hashCode();

View File

@@ -1,21 +1,30 @@
package com.muwire.core.download package com.muwire.core.download
import static org.junit.Assert.fail
import org.junit.After import org.junit.After
import org.junit.Before
import org.junit.Test import org.junit.Test
import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Personas
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.files.FileHasher import com.muwire.core.files.FileHasher
import static com.muwire.core.util.DataUtil.readTillRN import static com.muwire.core.util.DataUtil.readTillRN
import static com.muwire.core.util.DataUtil.encodeXHave
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class DownloadSessionTest { class DownloadSessionTest {
private EventBus eventBus
private File source, target private File source, target
private InfoHash infoHash private InfoHash infoHash
private Endpoint endpoint private Endpoint endpoint
private Pieces pieces, claimed private Pieces pieces
private String rootBase64 private String rootBase64
private DownloadSession session private DownloadSession session
@@ -24,6 +33,16 @@ class DownloadSessionTest {
private InputStream fromDownloader, fromUploader private InputStream fromDownloader, fromUploader
private OutputStream toDownloader, toUploader private OutputStream toDownloader, toUploader
private volatile boolean performed
private Set<Integer> available = new ConcurrentHashSet<>()
private volatile IOException thrown
@Before
public void setUp() {
eventBus = new EventBus()
}
private void initSession(int size, def claimedPieces = []) { private void initSession(int size, def claimedPieces = []) {
Random r = new Random() Random r = new Random()
byte [] content = new byte[size] byte [] content = new byte[size]
@@ -48,8 +67,7 @@ class DownloadSessionTest {
else else
nPieces = size / pieceSize + 1 nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces) pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces) claimedPieces.each {pieces.claimed.set(it)}
claimedPieces.each {claimed.markDownloaded(it)}
fromDownloader = new PipedInputStream() fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream() fromUploader = new PipedInputStream()
@@ -57,12 +75,20 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader) toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null) endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size) session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
downloadThread = new Thread( { session.request() } as Runnable) downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true) downloadThread.setDaemon(true)
downloadThread.start() downloadThread.start()
} }
private void perform() {
try {
performed = session.request()
} catch (IOException e) {
thrown = e
}
}
@After @After
public void teardown() { public void teardown() {
source?.delete() source?.delete()
@@ -77,6 +103,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader) assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader) readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -88,6 +115,9 @@ class DownloadSessionTest {
assert pieces.isComplete() assert pieces.isComplete()
assert target.bytes == source.bytes assert target.bytes == source.bytes
assert performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -99,6 +129,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader) readTillRN(fromDownloader)
readTillRN(fromDownloader) readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes) toDownloader.write("200 OK\r\n".bytes)
@@ -109,6 +140,9 @@ class DownloadSessionTest {
Thread.sleep(150) Thread.sleep(150)
assert pieces.isComplete() assert pieces.isComplete()
assert target.bytes == source.bytes assert target.bytes == source.bytes
assert performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -126,6 +160,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) || assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize)) (start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
readTillRN(fromDownloader) readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader) assert "" == readTillRN(fromDownloader)
@@ -139,6 +174,9 @@ class DownloadSessionTest {
Thread.sleep(150) Thread.sleep(150)
assert !pieces.isComplete() assert !pieces.isComplete()
assert 1 == pieces.donePieces() assert 1 == pieces.donePieces()
assert performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -146,7 +184,10 @@ class DownloadSessionTest {
initSession(20, [0]) initSession(20, [0])
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
downloadThread.join(100) downloadThread.join(100)
assert 100 > (System.currentTimeMillis() - now) assert 100 >= (System.currentTimeMillis() - now)
assert !performed
assert available.isEmpty()
assert thrown == null
} }
@Test @Test
@@ -154,7 +195,7 @@ class DownloadSessionTest {
int pieceSize = FileHasher.getPieceSize(1) int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10 int size = (1 << pieceSize) * 10
initSession(size, [1,2,3,4,5,6,7,8,9]) initSession(size, [1,2,3,4,5,6,7,8,9])
assert !claimed.isMarked(0) assert !pieces.claimed.get(0)
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader) String range = readTillRN(fromDownloader)
@@ -162,7 +203,131 @@ class DownloadSessionTest {
int start = Integer.parseInt(matcher[0][1]) int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2]) int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0) assert pieces.claimed.get(0)
assert start == 0 && end == (1 << pieceSize) - 1 assert start == 0 && end == (1 << pieceSize) - 1
} }
@Test
public void test416NoHave() {
initSession(20)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert !performed
assert available.isEmpty()
assert thrown != null
}
@Test
public void test416Have() {
initSession(20)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n".bytes)
toDownloader.write("X-Have: ${encodeXHave([0], 1)}\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert performed
assert available.contains(0)
assert thrown == null
}
@Test
public void test416Have2Pieces() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) + 1
initSession(size)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n".bytes)
toDownloader.write("X-Have: ${encodeXHave([1], 2)}\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert performed
assert available.contains(1)
assert thrown == null
}
@Test
public void test200TwoPieces1Available() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 9 + 1
initSession(size)
Set<String> headers = readAllHeaders(fromDownloader)
def matcher = null
headers.each {
if (it.startsWith("Range"))
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
}
assert matcher.groupCount() > 0
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
if (start == 0)
fail("inconlcusive")
toDownloader.write("416 don't have it \r\n".bytes)
toDownloader.write("X-Have: ${encodeXHave([0],2)}\r\n\r\n".bytes)
toDownloader.flush()
downloadThread.join()
assert performed
performed = false
assert available.contains(0)
assert thrown == null
// request same session
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
Thread.sleep(150)
headers = readAllHeaders(fromDownloader)
matcher = null
headers.each {
if (it.startsWith("Range"))
matcher = (it =~ /^Range: (\d+)-(\d+)$/)
}
assert matcher.groupCount() > 0
start = Integer.parseInt(matcher[0][1])
end = Integer.parseInt(matcher[0][2])
assert start == 0
}
@Test
public void testXAlt() throws Exception {
Personas personas = new Personas()
def sources = []
def listener = new Object() {
public void onSourceDiscoveredEvent(SourceDiscoveredEvent e) {
sources << e.source
}
}
eventBus.register(SourceDiscoveredEvent.class, listener)
initSession(20)
readAllHeaders(fromDownloader)
toDownloader.write("416 don't have it\r\n".bytes)
toDownloader.write("X-Alt: ${personas.persona1.toBase64()},${personas.persona2.toBase64()}\r\n\r\n".bytes)
toDownloader.flush()
Thread.sleep(150)
assert sources.contains(personas.persona1)
assert sources.contains(personas.persona2)
assert 2 == sources.size()
}
private static Set<String> readAllHeaders(InputStream is) {
Set<String> rv = new HashSet<>()
String header
while((header = readTillRN(is)) != "")
rv.add(header)
rv
}
} }

View File

@@ -16,7 +16,7 @@ class PiecesTest {
public void testSinglePiece() { public void testSinglePiece() {
pieces = new Pieces(1) pieces = new Pieces(1)
assert !pieces.isComplete() assert !pieces.isComplete()
assert pieces.getRandomPiece() == 0 assert pieces.claim() == 0
pieces.markDownloaded(0) pieces.markDownloaded(0)
assert pieces.isComplete() assert pieces.isComplete()
} }
@@ -25,13 +25,28 @@ class PiecesTest {
public void testTwoPieces() { public void testTwoPieces() {
pieces = new Pieces(2) pieces = new Pieces(2)
assert !pieces.isComplete() assert !pieces.isComplete()
int piece = pieces.getRandomPiece() int piece = pieces.claim()
assert piece == 0 || piece == 1 assert piece == 0 || piece == 1
pieces.markDownloaded(piece) pieces.markDownloaded(piece)
assert !pieces.isComplete() assert !pieces.isComplete()
int piece2 = pieces.getRandomPiece() int piece2 = pieces.claim()
assert piece != piece2 assert piece != piece2
pieces.markDownloaded(piece2) pieces.markDownloaded(piece2)
assert pieces.isComplete() assert pieces.isComplete()
} }
@Test
public void testClaimAvailable() {
pieces = new Pieces(2)
int claimed = pieces.claim([0].toSet())
assert claimed == 0
assert -1 == pieces.claim([0].toSet())
}
@Test
public void testClaimNoneAvailable() {
pieces = new Pieces(20)
int claimed = pieces.claim()
assert -1 == pieces.claim([claimed].toSet())
}
} }

View File

@@ -26,7 +26,7 @@ class FileHasherTest extends GroovyTestCase {
void testPieceSize() { void testPieceSize() {
assert 17 == FileHasher.getPieceSize(1000000) assert 17 == FileHasher.getPieceSize(1000000)
assert 17 == FileHasher.getPieceSize(100000000) assert 17 == FileHasher.getPieceSize(100000000)
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE) assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, { shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE) FileHasher.getPieceSize(Long.MAX_VALUE)
} }

View File

@@ -27,6 +27,7 @@ class HasherServiceTest {
hasher = new FileHasher() hasher = new FileHasher()
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings())) service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
eventBus.register(FileHashedEvent.class, listener) eventBus.register(FileHashedEvent.class, listener)
eventBus.register(FileSharedEvent.class, service)
service.start() service.start()
} }

View File

@@ -78,7 +78,7 @@ class PersisterServiceLoadingTest {
persisted.write json persisted.write json
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start() ps.onUILoadedEvent(null)
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 1 assert listener.publishedFiles.size() == 1
@@ -121,7 +121,7 @@ class PersisterServiceLoadingTest {
persisted.write json persisted.write json
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start() ps.onUILoadedEvent(null)
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 1 assert listener.publishedFiles.size() == 1
@@ -163,7 +163,7 @@ class PersisterServiceLoadingTest {
persisted.append "$json2\n" persisted.append "$json2\n"
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start() ps.onUILoadedEvent(null)
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 2 assert listener.publishedFiles.size() == 2
@@ -195,7 +195,7 @@ class PersisterServiceLoadingTest {
persisted.write json1 persisted.write json1
PersisterService ps = new PersisterService(persisted, eventBus, 100, null) PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start() ps.onUILoadedEvent(null)
Thread.sleep(2000) Thread.sleep(2000)
assert listener.publishedFiles.size() == 1 assert listener.publishedFiles.size() == 1

View File

@@ -58,7 +58,7 @@ class PersisterServiceSavingTest {
sf = new SharedFile(f, ih, 0) sf = new SharedFile(f, ih, 0)
ps = new PersisterService(persisted, eventBus, 100, fileSource) ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start() ps.onUILoadedEvent(null)
Thread.sleep(1500) Thread.sleep(1500)
JsonSlurper jsonSlurper = new JsonSlurper() JsonSlurper jsonSlurper = new JsonSlurper()
@@ -77,7 +77,7 @@ class PersisterServiceSavingTest {
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2])) sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
ps = new PersisterService(persisted, eventBus, 100, fileSource) ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start() ps.onUILoadedEvent(null)
Thread.sleep(1500) Thread.sleep(1500)
JsonSlurper jsonSlurper = new JsonSlurper() JsonSlurper jsonSlurper = new JsonSlurper()

View File

@@ -83,4 +83,11 @@ class SearchIndexTest {
assert found.size() == 1 assert found.size() == 1
assert found.contains("b c.d") assert found.contains("b c.d")
} }
@Test
void testDuplicateTerm() {
initIndex(["MuWire-0.3.3.jar"])
def found = index.search(["muwire", "0", "3", "jar"])
assert found.size() == 1
}
} }

View File

@@ -9,18 +9,18 @@ import com.muwire.core.InfoHash
class RequestParsingTest { class RequestParsingTest {
Request request ContentRequest request
private void fromString(String requestString) { private void fromString(String requestString) {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII)) def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
request = Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is) request = Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
} }
private static void failed(String requestString) { private static void failed(String requestString) {
try { try {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII)) def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is) Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
assert false assert false
} catch (IOException expected) {} } catch (IOException expected) {}
} }

View File

@@ -19,7 +19,7 @@ class UploaderTest {
InputStream is InputStream is
OutputStream os OutputStream os
Request request ContentRequest request
Uploader uploader Uploader uploader
byte[] inFile byte[] inFile
@@ -52,7 +52,7 @@ class UploaderTest {
} }
private void startUpload() { private void startUpload() {
uploader = new Uploader(file, request, endpoint) uploader = new ContentUploader(file, request, endpoint)
uploadThread = new Thread(uploader.respond() as Runnable) uploadThread = new Thread(uploader.respond() as Runnable)
uploadThread.setDaemon(true) uploadThread.setDaemon(true)
uploadThread.start() uploadThread.start()
@@ -77,7 +77,7 @@ class UploaderTest {
@Test @Test
public void testSmallFile() { public void testSmallFile() {
fillFile(20) fillFile(20)
request = new Request(range : new Range(0,19)) request = new ContentRequest(range : new Range(0,19))
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 0-19" == readUntilRN() assert "Content-Range: 0-19" == readUntilRN()
@@ -92,7 +92,7 @@ class UploaderTest {
@Test @Test
public void testRequestMiddle() { public void testRequestMiddle() {
fillFile(20) fillFile(20)
request = new Request(range : new Range(5,15)) request = new ContentRequest(range : new Range(5,15))
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 5-15" == readUntilRN() assert "Content-Range: 5-15" == readUntilRN()
@@ -108,7 +108,7 @@ class UploaderTest {
@Test @Test
public void testOutOfRange() { public void testOutOfRange() {
fillFile(20) fillFile(20)
request = new Request(range : new Range(0,20)) request = new ContentRequest(range : new Range(0,20))
startUpload() startUpload()
assert "416 Range Not Satisfiable" == readUntilRN() assert "416 Range Not Satisfiable" == readUntilRN()
assert "" == readUntilRN() assert "" == readUntilRN()
@@ -118,7 +118,7 @@ class UploaderTest {
public void testLargeFile() { public void testLargeFile() {
final int length = 0x1 << 14 final int length = 0x1 << 14
fillFile(length) fillFile(length)
request = new Request(range : new Range(0, length - 1)) request = new ContentRequest(range : new Range(0, length - 1))
startUpload() startUpload()
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()

View File

@@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
### Mesh management ### Mesh management
Download mesh management is identical to Gnutella, except instead of ip addresses MuWire personas are used. [More information](http://rfc-gnutella.sourceforge.net/developer/tmp/download-mesh.html) Download mesh management is a simplified version of Gnutella's "Alternate Location" system. For more information see the "download-mesh" document.
### In-Network updates ### In-Network updates

15
doc/download-mesh.md Normal file
View File

@@ -0,0 +1,15 @@
# Download Mesh / Partial Sharing
MuWire uses a system similar to Gnutella's "Alternate Location" download mesh management system, however it is simplified to account for I2P's strengths and borrows a bit from BitTorrent's "Have" message.
### "X-Have" header
With every request a downloader makes it sends an "X-Have" header containing the Base64-encoded representation of a bitfield where bits set to 1 represent pieces of the file that the downloader already has. To make partial file sharing possible, if the uploader does not have the complete file it also sends this header in every response. If the header is missing it is assumed the uploader has the complete file.
### "X-Alt" header
The uploader can recommend other uploaders to the downloader via the "X-Alt" header. The format of this header is a comma-separated list of Base64-encoded Personas that have previously reported having at least one piece of the file to the uploader via the "X-Have" header.
### Differences from Gnutella
Unlike Gnutella the uploader is the sole repository where possible sources of the file are tracked. There is no negative "X-Nalt" header to prevent attacking the download mesh by mass downvoting of sources.

View File

@@ -1,5 +1,8 @@
group = com.muwire group = com.muwire
version = 0.2.4 version = 0.3.6
groovyVersion = 2.4.15 groovyVersion = 2.4.15
slf4jVersion = 1.7.25 slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4 spockVersion = 1.1-groovy-2.4
sourceCompatibility=1.8
targetCompatibility=1.8

View File

@@ -17,6 +17,8 @@ import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
@@ -67,7 +69,9 @@ class MainFrameController {
// this can be improved a lot // this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ") def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
def terms = replaced.split(" ") def terms = replaced.split(" ")
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true) def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
} }
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,
@@ -118,7 +122,10 @@ class MainFrameController {
void download() { void download() {
def result = selectedResult() def result = selectedResult()
if (result == null) if (result == null)
return // TODO disable button return
if (!model.canDownload(result.infohash))
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
@@ -126,8 +133,9 @@ class MainFrameController {
def group = selected.getClientProperty("mvc-group") def group = selected.getClientProperty("mvc-group")
def resultsBucket = group.model.hashBucket[result.infohash] def resultsBucket = group.model.hashBucket[result.infohash]
def sources = group.model.sourcesBucket[result.infohash]
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file)) core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file))
} }
@ControllerAction @ControllerAction
@@ -150,6 +158,7 @@ class MainFrameController {
void cancel() { void cancel() {
def downloader = model.downloads[selectedDownload()].downloader def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel() downloader.cancel()
model.downloadInfoHashes.remove(downloader.getInfoHash())
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader)) core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
} }
@@ -157,6 +166,14 @@ class MainFrameController {
void resume() { void resume() {
def downloader = model.downloads[selectedDownload()].downloader def downloader = model.downloads[selectedDownload()].downloader
downloader.resume() downloader.resume()
core.eventBus.publish(new UIDownloadResumedEvent())
}
@ControllerAction
void pause() {
def downloader = model.downloads[selectedDownload()].downloader
downloader.pause()
core.eventBus.publish(new UIDownloadPausedEvent())
} }
private void markTrust(String tableName, TrustLevel level, def list) { private void markTrust(String tableName, TrustLevel level, def list) {
@@ -190,6 +207,13 @@ class MainFrameController {
println "unsharing selected files" println "unsharing selected files"
} }
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
}
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e-> application.addPropertyChangeListener("core", {e->
core = e.getNewValue() core = e.getNewValue()

View File

@@ -77,9 +77,9 @@ class OptionsController {
model.font = text model.font = text
uiSettings.font = text uiSettings.font = text
boolean showMonitor = view.monitorCheckbox.model.isSelected() // boolean showMonitor = view.monitorCheckbox.model.isSelected()
model.showMonitor = showMonitor // model.showMonitor = showMonitor
uiSettings.showMonitor = showMonitor // uiSettings.showMonitor = showMonitor
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected() boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
model.clearCancelledDownloads = clearCancelledDownloads model.clearCancelledDownloads = clearCancelledDownloads
@@ -93,9 +93,9 @@ class OptionsController {
model.excludeLocalResult = excludeLocalResult model.excludeLocalResult = excludeLocalResult
uiSettings.excludeLocalResult = excludeLocalResult uiSettings.excludeLocalResult = excludeLocalResult
boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected() // boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
model.showSearchHashes = showSearchHashes // model.showSearchHashes = showSearchHashes
uiSettings.showSearchHashes = showSearchHashes // uiSettings.showSearchHashes = showSearchHashes
File uiSettingsFile = new File(core.home, "gui.properties") File uiSettingsFile = new File(core.home, "gui.properties")
uiSettingsFile.withOutputStream { uiSettingsFile.withOutputStream {

View File

@@ -1,3 +1,4 @@
import griffon.core.GriffonApplication import griffon.core.GriffonApplication
import griffon.core.env.Metadata import griffon.core.env.Metadata
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -97,6 +98,9 @@ class Ready extends AbstractLifecycleHandler {
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE) "Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0) System.exit(0)
} }
Runtime.getRuntime().addShutdownHook({
core.shutdown()
})
core.startServices() core.startServices()
application.context.put("muwire-settings", props) application.context.put("muwire-settings", props)
application.context.put("core",core) application.context.put("core",core)
@@ -104,12 +108,6 @@ class Ready extends AbstractLifecycleHandler {
it.propertyChange(new PropertyChangeEvent(this, "core", null, core)) it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
} }
if (props.sharedFiles != null) {
props.sharedFiles.split(",").each {
core.eventBus.publish(new FileSharedEvent(file : new File(it)))
}
}
core.eventBus.publish(new UILoadedEvent()) core.eventBus.publish(new UILoadedEvent())
} }
} }

View File

@@ -9,7 +9,9 @@ import javax.swing.JTable
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.RouterDisconnectedEvent
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.connection.DisconnectionEvent
@@ -19,6 +21,7 @@ import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.search.QueryEvent import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultBatchEvent import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
@@ -53,6 +56,7 @@ class MainFrameModel {
def downloads = [] def downloads = []
def uploads = [] def uploads = []
def shared = [] def shared = []
def watched = []
def connectionList = [] def connectionList = []
def searches = new LinkedList() def searches = new LinkedList()
def trusted = [] def trusted = []
@@ -60,12 +64,17 @@ class MainFrameModel {
@Observable int connections @Observable int connections
@Observable String me @Observable String me
@Observable boolean searchButtonsEnabled @Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled
@Observable boolean cancelButtonEnabled @Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled @Observable boolean retryButtonEnabled
@Observable boolean pauseButtonEnabled
@Observable String resumeButtonText
private final Set<InfoHash> infoHashes = new HashSet<>() private final Set<InfoHash> infoHashes = new HashSet<>()
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
volatile Core core volatile Core core
private long lastRetryTime = System.currentTimeMillis() private long lastRetryTime = System.currentTimeMillis()
@@ -128,9 +137,13 @@ class MainFrameModel {
core.eventBus.register(QueryEvent.class, this) core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this) core.eventBus.register(UpdateAvailableEvent.class, this)
core.eventBus.register(FileDownloadedEvent.class, this) core.eventBus.register(FileDownloadedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(RouterDisconnectedEvent.class, this)
timer.schedule({ timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval if (core.shutdown.get())
return
int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) { if (retryInterval > 0) {
retryInterval *= 60000 retryInterval *= 60000
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
@@ -153,6 +166,12 @@ class MainFrameModel {
runInsideUIAsync { runInsideUIAsync {
trusted.addAll(core.trustService.good.values()) trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values()) distrusted.addAll(core.trustService.bad.values())
watched.addAll(core.muOptions.watchedDirectories)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
resumeButtonText = "Retry"
} }
}) })
@@ -171,6 +190,7 @@ class MainFrameModel {
void onDownloadStartedEvent(DownloadStartedEvent e) { void onDownloadStartedEvent(DownloadStartedEvent e) {
runInsideUIAsync { runInsideUIAsync {
downloads << e downloads << e
downloadInfoHashes.add(e.downloader.infoHash)
} }
} }
@@ -232,6 +252,17 @@ class MainFrameModel {
} }
} }
void onFileUnsharedEvent(FileUnsharedEvent e) {
InfoHash infohash = e.unsharedFile.infoHash
if (!infoHashes.remove(infohash))
return
runInsideUIAsync {
shared.remove(e.unsharedFile)
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
}
}
void onUploadEvent(UploadEvent e) { void onUploadEvent(UploadEvent e) {
runInsideUIAsync { runInsideUIAsync {
uploads << e.uploader uploads << e.uploader
@@ -259,8 +290,10 @@ class MainFrameModel {
updateTablePreservingSelection("trusted-table") updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table") updateTablePreservingSelection("distrusted-table")
results.values().each { results.values().each { MVCGroup group ->
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged() if (group.alive) {
group.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
}
} }
} }
} }
@@ -312,6 +345,14 @@ class MainFrameModel {
} }
} }
void onRouterDisconnectedEvent(RouterDisconnectedEvent e) {
runInsideUIAsync {
JOptionPane.showMessageDialog(null, "MuWire lost connection to the I2P router and will now exit.",
"Connection to I2P router lost", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
}
void onFileDownloadedEvent(FileDownloadedEvent e) { void onFileDownloadedEvent(FileDownloadedEvent e) {
if (!core.muOptions.shareDownloadedFiles) if (!core.muOptions.shareDownloadedFiles)
return return
@@ -340,4 +381,8 @@ class MainFrameModel {
return destination == other.destination return destination == other.destination
} }
} }
boolean canDownload(InfoHash hash) {
!downloadInfoHashes.contains(hash)
}
} }

View File

@@ -23,6 +23,7 @@ class SearchTabModel {
String uuid String uuid
def results = [] def results = []
def hashBucket = [:] def hashBucket = [:]
def sourcesBucket = [:]
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
@@ -37,7 +38,7 @@ class SearchTabModel {
void handleResult(UIResultEvent e) { void handleResult(UIResultEvent e) {
if (uiSettings.excludeLocalResult && if (uiSettings.excludeLocalResult &&
e.sender == core.me) core.fileManager.rootToFiles.containsKey(e.infohash))
return return
runInsideUIAsync { runInsideUIAsync {
def bucket = hashBucket.get(e.infohash) def bucket = hashBucket.get(e.infohash)
@@ -47,6 +48,13 @@ class SearchTabModel {
} }
bucket << e bucket << e
Set sourceBucket = sourcesBucket.get(e.infohash)
if (sourceBucket == null) {
sourceBucket = new HashSet()
sourcesBucket.put(e.infohash, sourceBucket)
}
sourceBucket.addAll(e.sources)
results << e results << e
JTable table = builder.getVariable("results-table") JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged() table.model.fireTableDataChanged()
@@ -56,13 +64,22 @@ class SearchTabModel {
void handleResultBatch(UIResultEvent[] batch) { void handleResultBatch(UIResultEvent[] batch) {
runInsideUIAsync { runInsideUIAsync {
batch.each { batch.each {
if (uiSettings.excludeLocalResult && it.sender == core.me) if (uiSettings.excludeLocalResult &&
core.fileManager.rootToFiles.containsKey(it.infohash))
return return
def bucket = hashBucket.get(it.infohash) def bucket = hashBucket.get(it.infohash)
if (bucket == null) { if (bucket == null) {
bucket = [] bucket = []
hashBucket[it.infohash] = bucket hashBucket[it.infohash] = bucket
} }
Set sourceBucket = sourcesBucket.get(it.infohash)
if (sourceBucket == null) {
sourceBucket = new HashSet()
sourcesBucket.put(it.infohash, sourceBucket)
}
sourceBucket.addAll(it.sources)
bucket << it bucket << it
results << it results << it
} }

View File

@@ -1,6 +1,7 @@
package com.muwire.gui package com.muwire.gui
import griffon.core.artifact.GriffonView import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -37,6 +38,7 @@ import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonView) @ArtifactProviderFor(GriffonView)
class MainFrameView { class MainFrameView {
@@ -45,6 +47,8 @@ class MainFrameView {
@MVCMember @Nonnull @MVCMember @Nonnull
MainFrameModel model MainFrameModel model
@Inject Metadata metadata
def downloadsTable def downloadsTable
def lastDownloadSortEvent def lastDownloadSortEvent
def lastSharedSortEvent def lastSharedSortEvent
@@ -54,7 +58,8 @@ class MainFrameView {
builder.with { builder.with {
application(size : [1024,768], id: 'main-frame', application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null, locationRelativeTo : null,
title: application.configuration['application.title'], title: application.configuration['application.title'] + " " +
metadata["application.version"] + " revision " + metadata["build.revision"],
iconImage: imageIcon('/griffon-icon-48x48.png').image, iconImage: imageIcon('/griffon-icon-48x48.png').image,
iconImages: [imageIcon('/griffon-icon-48x48.png').image, iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image, imageIcon('/griffon-icon-32x32.png').image,
@@ -105,9 +110,9 @@ class MainFrameView {
borderLayout() borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER) tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction) button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction) button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
} }
} }
panel (constraints : JSplitPane.BOTTOM) { panel (constraints : JSplitPane.BOTTOM) {
@@ -130,8 +135,9 @@ class MainFrameView {
} }
} }
panel (constraints : BorderLayout.SOUTH) { panel (constraints : BorderLayout.SOUTH) {
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction ) button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction) button(text: bind { model.resumeButtonText }, enabled : bind {model.retryButtonEnabled}, resumeAction)
} }
} }
} }
@@ -139,16 +145,32 @@ class MainFrameView {
panel (constraints: "uploads window"){ panel (constraints: "uploads window"){
gridLayout(cols : 1, rows : 2) gridLayout(cols : 1, rows : 2)
panel { panel {
borderLayout() gridLayout(cols : 2, rows : 1)
panel (constraints : BorderLayout.NORTH) { panel {
button(text : "Click here to share files", actionPerformed : shareFiles) borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Add directories to watch", actionPerformed : watchDirectories)
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "watched-directories-table", autoCreateRowSorter: true) {
tableModel(list : model.watched) {
closureColumn(header: "Watched Directories", type : String, read : { it })
}
}
}
} }
scrollPane ( constraints : BorderLayout.CENTER) { panel {
table(id : "shared-files-table", autoCreateRowSorter: true) { borderLayout()
tableModel(list : model.shared) { panel (constraints : BorderLayout.NORTH) {
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()}) button(text : "Share files", actionPerformed : shareFiles)
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() }) }
} scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.file.length() })
}
}
} }
} }
} }
@@ -274,6 +296,7 @@ class MainFrameView {
if (selectedRow < 0) { if (selectedRow < 0) {
model.cancelButtonEnabled = false model.cancelButtonEnabled = false
model.retryButtonEnabled = false model.retryButtonEnabled = false
model.pauseButtonEnabled = false
return return
} }
def downloader = model.downloads[selectedRow]?.downloader def downloader = model.downloads[selectedRow]?.downloader
@@ -284,15 +307,25 @@ class MainFrameView {
case Downloader.DownloadState.DOWNLOADING : case Downloader.DownloadState.DOWNLOADING :
case Downloader.DownloadState.HASHLIST: case Downloader.DownloadState.HASHLIST:
model.cancelButtonEnabled = true model.cancelButtonEnabled = true
model.pauseButtonEnabled = true
model.retryButtonEnabled = false model.retryButtonEnabled = false
break break
case Downloader.DownloadState.FAILED: case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = true model.cancelButtonEnabled = true
model.retryButtonEnabled = true model.retryButtonEnabled = true
model.resumeButtonText = "Retry"
model.pauseButtonEnabled = false
break
case Downloader.DownloadState.PAUSED:
model.cancelButtonEnabled = true
model.retryButtonEnabled = true
model.resumeButtonText = "Resume"
model.pauseButtonEnabled = false
break break
default: default:
model.cancelButtonEnabled = false model.cancelButtonEnabled = false
model.retryButtonEnabled = false model.retryButtonEnabled = false
model.pauseButtonEnabled = false
} }
}) })
@@ -400,21 +433,32 @@ class MainFrameView {
int selected = selectedDownloaderRow() int selected = selectedDownloaderRow()
if (selected < 0) if (selected < 0)
return return
boolean pauseEnabled = false
boolean cancelEnabled = false boolean cancelEnabled = false
boolean retryEnabled = false boolean retryEnabled = false
String resumeText = "Retry"
Downloader downloader = model.downloads[selected].downloader Downloader downloader = model.downloads[selected].downloader
switch(downloader.currentState) { switch(downloader.currentState) {
case Downloader.DownloadState.DOWNLOADING: case Downloader.DownloadState.DOWNLOADING:
case Downloader.DownloadState.HASHLIST: case Downloader.DownloadState.HASHLIST:
case Downloader.DownloadState.CONNECTING: case Downloader.DownloadState.CONNECTING:
pauseEnabled = true
cancelEnabled = true cancelEnabled = true
retryEnabled = false retryEnabled = false
break break
case Downloader.DownloadState.FAILED: case Downloader.DownloadState.FAILED:
pauseEnabled = false
cancelEnabled = true cancelEnabled = true
retryEnabled = true retryEnabled = true
break break
case Downloader.DownloadState.PAUSED:
pauseEnabled = false
cancelEnabled = true
retryEnabled = true
resumeText = "Resume"
break
default : default :
pauseEnabled = false
cancelEnabled = false cancelEnabled = false
retryEnabled = false retryEnabled = false
} }
@@ -429,6 +473,12 @@ class MainFrameView {
}) })
menu.add(copyHashToClipboard) menu.add(copyHashToClipboard)
if (pauseEnabled) {
JMenuItem pause = new JMenuItem("Pause")
pause.addActionListener({mvcGroup.controller.pause()})
menu.add(pause)
}
if (cancelEnabled) { if (cancelEnabled) {
JMenuItem cancel = new JMenuItem("Cancel") JMenuItem cancel = new JMenuItem("Cancel")
cancel.addActionListener({mvcGroup.controller.cancel()}) cancel.addActionListener({mvcGroup.controller.cancel()})
@@ -436,7 +486,7 @@ class MainFrameView {
} }
if (retryEnabled) { if (retryEnabled) {
JMenuItem retry = new JMenuItem("Retry") JMenuItem retry = new JMenuItem(resumeText)
retry.addActionListener({mvcGroup.controller.resume()}) retry.addActionListener({mvcGroup.controller.resume()})
menu.add(retry) menu.add(retry)
} }
@@ -466,11 +516,26 @@ class MainFrameView {
def shareFiles = { def shareFiles = {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share") chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES) chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) { if (rv == JFileChooser.APPROVE_OPTION) {
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile())) model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
} }
} }
def watchDirectories = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select directory to watch")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile()
model.watched << f.getAbsolutePath()
application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath()
mvcGroup.controller.saveMuWireSettings()
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
model.core.eventBus.publish(new FileSharedEvent(file : f))
}
}
} }

View File

@@ -89,16 +89,16 @@ class OptionsView {
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1)) lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2)) label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2)) fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3)) // label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3)) // monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4)) label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4)) clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5)) label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5)) clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6)) label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6)) excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7)) // label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7)) // showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
} }
buttonsPanel = builder.panel { buttonsPanel = builder.panel {
gridBagLayout() gridBagLayout()

View File

@@ -47,8 +47,9 @@ class SearchTabView {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) { resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) { tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')}) closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 50, type: Long, read : {row -> row.size}) closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size})
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()}) closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()}) closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row -> closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination).toString() model.core.trustService.getLevel(row.sender.destination).toString()
@@ -66,7 +67,13 @@ class SearchTabView {
def selectionModel = resultsTable.getSelectionModel() def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( { selectionModel.addListSelectionListener( {
mvcGroup.parentGroup.model.searchButtonsEnabled = true int row = resultsTable.getSelectedRow()
if (row < 0)
return
if (lastSortEvent != null)
row = resultsTable.rowSorter.convertRowIndexToModel(row)
mvcGroup.parentGroup.model.trustButtonsEnabled = true
mvcGroup.parentGroup.model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash)
}) })
} }
} }
@@ -105,25 +112,18 @@ class SearchTabView {
resultsTable.rowSorter.setSortsOnUpdates(true) resultsTable.rowSorter.setSortsOnUpdates(true)
JPopupMenu menu = new JPopupMenu()
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
resultsTable.addMouseListener(new MouseAdapter() { resultsTable.addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3) if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e) showPopupMenu(e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2) else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download() mvcGroup.parentGroup.controller.download()
} }
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3) if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e) showPopupMenu(e)
} }
}) })
} }
@@ -131,12 +131,21 @@ class SearchTabView {
def closeTab = { def closeTab = {
int index = parent.indexOfTab(searchTerms) int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index) parent.removeTabAt(index)
mvcGroup.parentGroup.model.searchButtonsEnabled = false mvcGroup.parentGroup.model.trustButtonsEnabled = false
mvcGroup.parentGroup.model.downloadActionEnabled = false
mvcGroup.destroy() mvcGroup.destroy()
} }
def showPopupMenu(JPopupMenu menu, MouseEvent e) { def showPopupMenu(MouseEvent e) {
println "showing popup menu" JPopupMenu menu = new JPopupMenu()
if (mvcGroup.parentGroup.model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.parentGroup.controller.download()})
menu.add(download)
}
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.add(copyHashToClipboard)
menu.show(e.getComponent(), e.getX(), e.getY()) menu.show(e.getComponent(), e.getX(), e.getY())
} }

View File

@@ -12,12 +12,12 @@ class UISettings {
UISettings(Properties props) { UISettings(Properties props) {
lnf = props.getProperty("lnf", "system") lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true")) showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "false"))
font = props.getProperty("font",null) font = props.getProperty("font",null)
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false")) clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","true"))
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false")) clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false")) excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false")) showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {

View File

@@ -0,0 +1,61 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= SEVERE
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = MuWire.log
java.util.logging.FileHandler.limit = 50000000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
# <level>: <log message> [<date/time>]
#
#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %2$s %5$s %6$s %n
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE

View File

@@ -0,0 +1,61 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= WARNING
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = MuWire.log
java.util.logging.FileHandler.limit = 50000000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
# <level>: <log message> [<date/time>]
#
#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %2$s %5$s %6$s %n
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE

View File

@@ -0,0 +1,61 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= INFO
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = MuWire.log
java.util.logging.FileHandler.limit = 50000000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
# <level>: <log message> [<date/time>]
#
#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %2$s %5$s %6$s %n
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE

View File

@@ -0,0 +1,62 @@
############################################################
# Default Logging Configuration File
#
# You can use a different file by specifying a filename
# with the java.util.logging.config.file system property.
# For example java -Djava.util.logging.config.file=myfile
############################################################
############################################################
# Global properties
############################################################
# "handlers" specifies a comma separated list of log Handler
# classes. These handlers will be installed during VM startup.
# Note that these classes must be on the system classpath.
# By default we only configure a ConsoleHandler, which will only
# show messages at the INFO and above levels.
handlers= java.util.logging.FileHandler
# To also add the FileHandler, use the following line instead.
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Default global logging level.
# This specifies which kinds of events are logged across
# all loggers. For any given facility this global level
# can be overriden by a facility specific level
# Note that the ConsoleHandler also has a separate level
# setting to limit messages printed to the console.
.level= SEVERE
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = MuWire.log
java.util.logging.FileHandler.limit = 50000000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Example to customize the SimpleFormatter output format
# to print one-line log message like this:
# <level>: <log message> [<date/time>]
#
#java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %2$s %5$s %6$s %n
############################################################
# Facility specific properties.
# Provides extra control for each logger.
############################################################
# For example, set the com.xyz.foo logger to only log SEVERE
# messages:
com.xyz.foo.level = SEVERE
com.muwire.core.level = FINE