Compare commits

..

118 Commits

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

10
TODO.md
View File

@ -4,10 +4,6 @@ Not in any particular order yet
### 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
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
@ -32,11 +28,13 @@ 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
##### Metadata editing and search
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items
* Detect if router is dead and show warning or exit
* Wrapper of some kind for in-place upgrades
* Download file sequentially
* Unsharing of files
* Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item)

View File

@ -4,6 +4,7 @@ import java.util.concurrent.CountDownLatch
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
@ -34,7 +35,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.2.4")
core = new Core(props, home, "0.3.9")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"
@ -72,7 +73,7 @@ class Cli {
Timer timer = new Timer("status-printer", true)
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)
def latch = new CountDownLatch(1)
@ -83,7 +84,8 @@ class Cli {
}
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
core.startServices()
core.eventBus.publish(new UILoadedEvent())
println "waiting for files to load"
latch.await()
// now we begin
@ -117,11 +119,11 @@ class Cli {
volatile int uploads
public void onUploadEvent(UploadEvent e) {
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) {
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
try {
core = new Core(props, home, "0.2.4")
core = new Core(props, home, "0.3.9")
} catch (Exception bad) {
bad.printStackTrace(System.out)
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 String SPLIT_PATTERN = "[\\.,_-]"
public static final String SPLIT_PATTERN = "[\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|]"
}

View File

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

View File

@ -1,6 +1,11 @@
package com.muwire.core
import java.util.stream.Collectors
import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class MuWireSettings {
@ -10,10 +15,9 @@ class MuWireSettings {
int updateCheckInterval
String nickname
File downloadLocation
String sharedFiles
CrawlerResponse crawlerResponse
boolean shareDownloadedFiles
boolean watchSharedDirectories
Set<String> watchedDirectories
MuWireSettings() {
this(new Properties())
@ -26,11 +30,16 @@ class MuWireSettings {
nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
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 {
@ -43,9 +52,14 @@ class MuWireSettings {
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
if (!watchedDirectories.isEmpty()) {
String encoded = watchedDirectories.stream().
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty("watchedDirectories", encoded)
}
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.files.FileDownloadedEvent
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 groovy.json.JsonBuilder
@ -14,24 +18,33 @@ import net.i2p.util.ConcurrentHashSet
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.UILoadedEvent
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
public class DownloadManager {
private final EventBus eventBus
private final TrustService trustService
private final MeshManager meshManager
private final MuWireSettings muSettings
private final I2PConnector connector
private final Executor executor
private final File incompletes, home
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.trustService = trustService
this.meshManager = meshManager
this.muSettings = muSettings
this.connector = connector
this.incompletes = new File(home,"incompletes")
this.home = home
@ -58,18 +71,30 @@ public class DownloadManager {
e.result.each {
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,
infohash, pieceSize, connector, destinations,
incompletes)
downloaders.add(downloader)
incompletes, pieces)
downloaders.put(infohash, downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
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()
}
@ -97,23 +122,54 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root)
}
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
downloaders.add(downloader)
downloader.download()
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
if (json.paused != null)
downloader.paused = json.paused
downloaders.put(infoHash, downloader)
downloader.readPieces()
if (!downloader.paused)
downloader.download()
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) {
downloaders.remove(e.downloader)
downloaders.remove(e.downloader.infoHash)
persistDownloaders()
}
private void persistDownloaders() {
File downloadsFile = new File(home,"downloads.json")
downloadsFile.withPrintWriter { writer ->
downloaders.each { downloader ->
downloaders.values().each { downloader ->
if (!downloader.cancelled) {
def json = [:]
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
@ -130,6 +186,8 @@ public class DownloadManager {
json.hashList = Base64.encode(infoHash.hashList)
else
json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused
writer.println(JsonOutput.toJson(json))
}
}
@ -137,7 +195,7 @@ public class DownloadManager {
}
public void shutdown() {
downloaders.each { it.stop() }
downloaders.values().each { it.stop() }
Downloader.executorService.shutdownNow()
}
}

View File

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

View File

@ -4,6 +4,9 @@ import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -15,13 +18,15 @@ import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.connection.I2PConnector
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@Log
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 static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
@ -43,18 +48,20 @@ public class Downloader {
private final Set<Destination> destinations
private final int nPieces
private final File piecesFile
private final File incompleteFile
final int pieceSizePow2
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 boolean piecesFileClosed
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) {
File incompletes, Pieces pieces) {
this.eventBus = eventBus
this.me = me
this.downloadManager = downloadManager
@ -64,17 +71,11 @@ public class Downloader {
this.connector = connector
this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2
int nPieces
if (length % pieceSize == 0)
nPieces = length / pieceSize
else
nPieces = length / pieceSize + 1
this.nPieces = nPieces
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
this.pieces = pieces
this.nPieces = pieces.nPieces
}
public synchronized InfoHash getInfoHash() {
@ -136,6 +137,9 @@ public class Downloader {
public DownloadState getCurrentState() {
if (cancelled)
return DownloadState.CANCELLED
if (paused)
return DownloadState.PAUSED
boolean allFinished = true
activeWorkers.values().each {
allFinished &= it.currentState == WorkerState.FINISHED
@ -175,11 +179,17 @@ public class Downloader {
public void cancel() {
cancelled = true
stop()
file.delete()
synchronized(piecesFile) {
piecesFileClosed = true
piecesFile.delete()
}
incompleteFile.delete()
pieces.clearAll()
}
public void pause() {
paused = true
stop()
}
void stop() {
@ -198,6 +208,8 @@ public class Downloader {
}
public void resume() {
paused = false
readPieces()
destinations.each { destination ->
def worker = activeWorkers.get(destination)
if (worker != null) {
@ -214,12 +226,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 {
private final Destination destination
private volatile WorkerState currentState
private volatile Thread downloadThread
private Endpoint endpoint
private volatile DownloadSession currentSession
private final Set<Integer> available = new HashSet<>()
DownloadWorker(Destination destination) {
this.destination = destination
@ -240,14 +261,16 @@ public class Downloader {
currentState = WorkerState.DOWNLOADING
boolean requestPerformed
while(!pieces.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, file, pieceSize, length)
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
endpoint, incompleteFile, pieceSize, length, available)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
successfulDestinations.add(endpoint.destination)
writePieces()
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
} finally {
currentState = WorkerState.FINISHED
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
@ -255,9 +278,15 @@ public class Downloader {
piecesFileClosed = true
piecesFile.delete()
}
try {
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
} catch (AtomicMoveNotSupportedException e) {
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
incompleteFile.delete()
}
eventBus.publish(
new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations),
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() {
def rv = []
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
@ -62,4 +74,13 @@ class Pieces {
synchronized int donePieces() {
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.search.UIResultEvent
import net.i2p.data.Destination
class UIDownloadEvent extends Event {
UIResultEvent[] result
Set<Destination> sources
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
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 {
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) {
f = f.getCanonicalFile()
if (f.isDirectory()) {
f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) }
} else {
if (f.length() == 0) {
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.Service
import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
@ -36,14 +37,14 @@ class PersisterService extends Service {
timer = new Timer("file persister", true)
}
void start() {
timer.schedule({load()} as TimerTask, 1)
}
void stop() {
timer.cancel()
}
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void load() {
if (location.exists() && location.isFile()) {
def slurper = new JsonSlurper()

View File

@ -5,9 +5,11 @@ import net.i2p.data.Destination
class Host {
private static final int MAX_FAILURES = 3
private static final int CLEAR_INTERVAL = 60 * 60 * 1000
final Destination destination
int failures,successes
long lastAttempt
public Host(Destination destination) {
this.destination = destination
@ -16,11 +18,13 @@ class Host {
synchronized void onConnect() {
failures = 0
successes++
lastAttempt = System.currentTimeMillis()
}
synchronized void onFailure() {
failures++
successes = 0
lastAttempt = System.currentTimeMillis()
}
synchronized boolean isFailed() {
@ -34,4 +38,8 @@ class Host {
synchronized void clearFailures() {
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.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null)
host.lastAttempt = entry.lastAttempt
if (allowHost(host))
hosts.put(dest, host)
}
@ -118,7 +120,7 @@ class HostCache extends Service {
}
private boolean allowHost(Host host) {
if (host.isFailed())
if (host.isFailed() && !host.canTryAgain())
return false
if (host.destination == myself)
return false
@ -143,6 +145,7 @@ class HostCache extends Service {
map.destination = dest.toBase64()
map.failures = host.failures
map.successes = host.successes
map.lastAttempt = host.lastAttempt
def json = JsonOutput.toJson(map)
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,101 @@
package com.muwire.core.mesh
import java.util.stream.Collectors
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import net.i2p.data.Base64
class MeshManager {
private static final int EXPIRATION = 60 * 60 * 1000
private final Map<InfoHash, Mesh> meshes = Collections.synchronizedMap(new HashMap<>())
private final FileManager fileManager
private final File home
MeshManager(FileManager fileManager, File home) {
this.fileManager = fileManager
this.home = home
load()
}
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)
save()
}
private void save() {
File meshFile = new File(home, "mesh.json")
synchronized(meshes) {
meshFile.withPrintWriter { writer ->
meshes.values().each { mesh ->
def json = [:]
json.timestamp = System.currentTimeMillis()
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
json.nPieces = mesh.pieces.nPieces
json.xHave = DataUtil.encodeXHave(mesh.pieces.downloaded, mesh.pieces.nPieces)
writer.println(JsonOutput.toJson(json))
}
}
}
}
private void load() {
File meshFile = new File(home, "mesh.json")
if (!meshFile.exists())
return
long now = System.currentTimeMillis()
JsonSlurper slurper = new JsonSlurper()
meshFile.eachLine {
def json = slurper.parseText(it)
if (now - json.timestamp > EXPIRATION)
return
InfoHash infoHash = new InfoHash(Base64.decode(json.infoHash))
Pieces pieces = new Pieces(json.nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
Mesh mesh = new Mesh(infoHash, pieces)
json.sources.each { source ->
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
mesh.sources.add(persona)
}
if (json.xHave != null)
DataUtil.decodeXHave(json.xHave).each { pieces.markDownloaded(it) }
if (!mesh.sources.isEmpty())
meshes.put(infoHash, mesh)
}
}
}

View File

@ -1,5 +1,7 @@
package com.muwire.core.search
import java.util.stream.Collectors
import javax.naming.directory.InvalidSearchControlsException
import com.muwire.core.InfoHash
@ -7,6 +9,7 @@ import com.muwire.core.Persona
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.data.Destination
class ResultsParser {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
@ -58,6 +61,7 @@ class ResultsParser {
size : size,
infohash : parsedIH,
pieceSize : pieceSize,
sources : Collections.emptySet(),
uuid : uuid)
} catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e)
@ -82,11 +86,17 @@ class ResultsParser {
if (infoHash.length != InfoHash.SIZE)
throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
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,
name : name,
size : size,
infohash : new InfoHash(infoHash),
pieceSize : pieceSize,
sources : sources,
uuid: uuid)
} catch (Exception 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.ThreadFactory
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.InfoHash
@ -54,12 +57,16 @@ class ResultsSender {
int pieceSize = it.getPieceSize()
if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length)
Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile)
suggested = it.sources
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
pieceSize : pieceSize,
uuid : uuid
uuid : uuid,
sources : suggested
)
eventBus.publish(uiResultEvent)
}
@ -77,46 +84,54 @@ class ResultsSender {
@Override
public void run() {
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null;
try {
endpoint = connector.connect(target)
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
me.write(os)
os.writeShort((short)results.length)
results.each {
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = oobInfohash ? 2 : 1
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
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)
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null;
try {
endpoint = connector.connect(target)
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
me.write(os)
os.writeShort((short)results.length)
results.each {
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
def baos = new ByteArrayOutputStream()
def daos = new DataOutputStream(baos)
daos.writeShort((short) name.length)
daos.write(name)
daos.flush()
String encodedName = Base64.encode(baos.toByteArray())
def obj = [:]
obj.type = "Result"
obj.version = oobInfohash ? 2 : 1
obj.name = encodedName
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
obj.size = it.getFile().length()
obj.pieceSize = it.getPieceSize()
if (!oobInfohash) {
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.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
os.flush()
} finally {
endpoint?.close()
}
os.flush()
} finally {
endpoint?.close()
} catch (Exception e) {
log.log(Level.WARNING, "problem sending results",e)
}
}
}

View File

@ -33,7 +33,10 @@ class SearchIndex {
private static String[] split(String source) {
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) {

View File

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

View File

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

View File

@ -5,33 +5,57 @@ import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets
import java.nio.file.Files
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.mesh.Mesh
import com.muwire.core.util.DataUtil
import net.i2p.data.Destination
class ContentUploader extends Uploader {
private final File file
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)
this.file = file
this.request = request
this.mesh = mesh
this.pieceSize = pieceSize
}
@Override
void respond() {
OutputStream os = endpoint.getOutputStream()
Range range = request.getRange()
if (range.start >= file.length() || range.end >= file.length()) {
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
boolean satisfiable = true
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()
return
}
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
try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
@ -50,6 +74,17 @@ class ContentUploader extends Uploader {
endpoint.getOutputStream().flush()
}
}
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
public String getName() {

View File

@ -5,6 +5,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.util.DataUtil
import groovy.util.logging.Log
import net.i2p.data.Base64
@ -48,8 +49,14 @@ class Request {
def decoded = Base64.decode(encoded)
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),
headers : headers, downloader : downloader)
headers : headers, downloader : downloader, have : have)
}
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.SharedFile
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.mesh.Mesh
import com.muwire.core.mesh.MeshManager
import groovy.util.logging.Log
import net.i2p.data.Base64
@ -15,12 +20,17 @@ import net.i2p.data.Base64
public class UploadManager {
private final EventBus eventBus
private final FileManager fileManager
private final MeshManager meshManager
private final DownloadManager downloadManager
public UploadManager() {}
public UploadManager(EventBus eventBus, FileManager fileManager) {
public UploadManager(EventBus eventBus, FileManager fileManager,
MeshManager meshManager, DownloadManager downloadManager) {
this.eventBus = eventBus
this.fileManager = fileManager
this.meshManager = meshManager
this.downloadManager = downloadManager
}
public void processGET(Endpoint e) throws IOException {
@ -44,8 +54,10 @@ public class UploadManager {
log.info("Responding to upload request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString)
InfoHash infoHash = new InfoHash(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"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
@ -61,13 +73,31 @@ public class UploadManager {
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) {
log.info("Downloader persona doesn't match their destination")
e.close()
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))
try {
uploader.respond()
@ -85,8 +115,10 @@ public class UploadManager {
log.info("Responding to hashlist request for root $infoHashString")
byte [] infoHashRoot = Base64.decode(infoHashString)
InfoHash infoHash = new InfoHash(infoHashRoot)
Downloader downloader = downloadManager.downloaders.get(infoHash)
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
if (sharedFiles == null || sharedFiles.isEmpty()) {
if (downloader == null && (sharedFiles == null || sharedFiles.isEmpty())) {
log.info "file not found"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
@ -102,13 +134,30 @@ public class UploadManager {
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) {
log.info("Downloader persona doesn't match their destination")
e.close()
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))
try {
uploader.respond()
@ -130,8 +179,10 @@ public class UploadManager {
log.info("Responding to upload request for root $infoHashString")
infoHashRoot = Base64.decode(infoHashString)
infoHash = new InfoHash(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"
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
e.getOutputStream().flush()
@ -153,7 +204,25 @@ public class UploadManager {
e.close()
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))
try {
uploader.respond()

View File

@ -4,6 +4,8 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants
import net.i2p.data.Base64
class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1
@ -79,4 +81,38 @@ class DataUtil {
}
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
}
public static Exception findRoot(Exception e) {
while(e.getCause() != null)
e = e.getCause()
e
}
}

View File

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

View File

@ -1,21 +1,30 @@
package com.muwire.core.download
import static org.junit.Assert.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
import com.muwire.core.EventBus
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.files.FileHasher
import static com.muwire.core.util.DataUtil.readTillRN
import static com.muwire.core.util.DataUtil.encodeXHave
import net.i2p.data.Base64
import net.i2p.util.ConcurrentHashSet
class DownloadSessionTest {
private EventBus eventBus
private File source, target
private InfoHash infoHash
private Endpoint endpoint
private Pieces pieces, claimed
private Pieces pieces
private String rootBase64
private DownloadSession session
@ -24,6 +33,16 @@ class DownloadSessionTest {
private InputStream fromDownloader, fromUploader
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 = []) {
Random r = new Random()
byte [] content = new byte[size]
@ -48,8 +67,7 @@ class DownloadSessionTest {
else
nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces)
claimedPieces.each {claimed.markDownloaded(it)}
claimedPieces.each {pieces.claimed.set(it)}
fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream()
@ -57,12 +75,20 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable)
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available)
downloadThread = new Thread( { perform() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
}
private void perform() {
try {
performed = session.request()
} catch (IOException e) {
thrown = e
}
}
@After
public void teardown() {
source?.delete()
@ -77,6 +103,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@ -88,6 +115,9 @@ class DownloadSessionTest {
assert pieces.isComplete()
assert target.bytes == source.bytes
assert performed
assert available.isEmpty()
assert thrown == null
}
@Test
@ -99,6 +129,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@ -109,6 +140,9 @@ class DownloadSessionTest {
Thread.sleep(150)
assert pieces.isComplete()
assert target.bytes == source.bytes
assert performed
assert available.isEmpty()
assert thrown == null
}
@Test
@ -126,6 +160,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
@ -139,6 +174,9 @@ class DownloadSessionTest {
Thread.sleep(150)
assert !pieces.isComplete()
assert 1 == pieces.donePieces()
assert performed
assert available.isEmpty()
assert thrown == null
}
@Test
@ -146,7 +184,10 @@ class DownloadSessionTest {
initSession(20, [0])
long now = System.currentTimeMillis()
downloadThread.join(100)
assert 100 > (System.currentTimeMillis() - now)
assert 100 >= (System.currentTimeMillis() - now)
assert !performed
assert available.isEmpty()
assert thrown == null
}
@Test
@ -154,7 +195,7 @@ class DownloadSessionTest {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10
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)
String range = readTillRN(fromDownloader)
@ -162,7 +203,131 @@ class DownloadSessionTest {
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0)
assert pieces.claimed.get(0)
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() {
pieces = new Pieces(1)
assert !pieces.isComplete()
assert pieces.getRandomPiece() == 0
assert pieces.claim() == 0
pieces.markDownloaded(0)
assert pieces.isComplete()
}
@ -25,13 +25,28 @@ class PiecesTest {
public void testTwoPieces() {
pieces = new Pieces(2)
assert !pieces.isComplete()
int piece = pieces.getRandomPiece()
int piece = pieces.claim()
assert piece == 0 || piece == 1
pieces.markDownloaded(piece)
assert !pieces.isComplete()
int piece2 = pieces.getRandomPiece()
int piece2 = pieces.claim()
assert piece != piece2
pieces.markDownloaded(piece2)
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() {
assert 17 == FileHasher.getPieceSize(1000000)
assert 17 == FileHasher.getPieceSize(100000000)
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE)
}

View File

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

View File

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

View File

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

View File

@ -83,4 +83,11 @@ class SearchIndexTest {
assert found.size() == 1
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 {
Request request
ContentRequest request
private void fromString(String requestString) {
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) {
try {
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
} catch (IOException expected) {}
}

View File

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

View File

@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
### 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

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
version = 0.2.4
version = 0.3.9
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
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.UIDownloadCancelledEvent
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.SearchEvent
import com.muwire.core.trust.TrustEvent
@ -67,7 +69,9 @@ class MainFrameController {
// this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
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,
replyTo: core.me.destination, receivedOn: core.me.destination,
@ -118,16 +122,20 @@ class MainFrameController {
void download() {
def result = selectedResult()
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 selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group")
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
@ -150,6 +158,7 @@ class MainFrameController {
void cancel() {
def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel()
model.downloadInfoHashes.remove(downloader.getInfoHash())
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
}
@ -157,6 +166,14 @@ class MainFrameController {
void resume() {
def downloader = model.downloads[selectedDownload()].downloader
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) {
@ -190,6 +207,13 @@ class MainFrameController {
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) {
application.addPropertyChangeListener("core", {e->
core = e.getNewValue()

View File

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

View File

@ -1,3 +1,4 @@
import griffon.core.GriffonApplication
import griffon.core.env.Metadata
import groovy.util.logging.Log
@ -97,6 +98,9 @@ class Ready extends AbstractLifecycleHandler {
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
Runtime.getRuntime().addShutdownHook({
core.shutdown()
})
core.startServices()
application.context.put("muwire-settings", props)
application.context.put("core",core)
@ -104,12 +108,6 @@ class Ready extends AbstractLifecycleHandler {
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())
}
}

View File

@ -9,7 +9,9 @@ import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.RouterDisconnectedEvent
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
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.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultBatchEvent
import com.muwire.core.search.UIResultEvent
@ -53,6 +56,7 @@ class MainFrameModel {
def downloads = []
def uploads = []
def shared = []
def watched = []
def connectionList = []
def searches = new LinkedList()
def trusted = []
@ -60,11 +64,16 @@ class MainFrameModel {
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
@Observable boolean pauseButtonEnabled
@Observable String resumeButtonText
private final Set<InfoHash> infoHashes = new HashSet<>()
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
volatile Core core
@ -128,9 +137,13 @@ class MainFrameModel {
core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
core.eventBus.register(FileDownloadedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
core.eventBus.register(RouterDisconnectedEvent.class, this)
timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (core.shutdown.get())
return
int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
long now = System.currentTimeMillis()
@ -153,6 +166,12 @@ class MainFrameModel {
runInsideUIAsync {
trusted.addAll(core.trustService.good.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) {
runInsideUIAsync {
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) {
runInsideUIAsync {
uploads << e.uploader
@ -259,8 +290,10 @@ class MainFrameModel {
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
results.values().each {
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
results.values().each { MVCGroup group ->
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) {
if (!core.muOptions.shareDownloadedFiles)
return
@ -340,4 +381,8 @@ class MainFrameModel {
return destination == other.destination
}
}
boolean canDownload(InfoHash hash) {
!downloadInfoHashes.contains(hash)
}
}

View File

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

View File

@ -1,6 +1,7 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64
@ -37,6 +38,7 @@ import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonView)
class MainFrameView {
@ -45,6 +47,8 @@ class MainFrameView {
@MVCMember @Nonnull
MainFrameModel model
@Inject Metadata metadata
def downloadsTable
def lastDownloadSortEvent
def lastSharedSortEvent
@ -54,7 +58,8 @@ class MainFrameView {
builder.with {
application(size : [1024,768], id: 'main-frame',
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,
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image,
@ -105,9 +110,9 @@ class MainFrameView {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
}
}
panel (constraints : JSplitPane.BOTTOM) {
@ -130,8 +135,9 @@ class MainFrameView {
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text: "Pause", enabled : bind {model.pauseButtonEnabled}, pauseAction)
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"){
gridLayout(cols : 1, rows : 2)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Click here to share files", actionPerformed : shareFiles)
gridLayout(cols : 2, rows : 1)
panel {
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) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() })
}
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Share files", actionPerformed : shareFiles)
}
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) {
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
model.pauseButtonEnabled = false
return
}
def downloader = model.downloads[selectedRow]?.downloader
@ -284,15 +307,25 @@ class MainFrameView {
case Downloader.DownloadState.DOWNLOADING :
case Downloader.DownloadState.HASHLIST:
model.cancelButtonEnabled = true
model.pauseButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = 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
default:
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
model.pauseButtonEnabled = false
}
})
@ -400,21 +433,32 @@ class MainFrameView {
int selected = selectedDownloaderRow()
if (selected < 0)
return
boolean pauseEnabled = false
boolean cancelEnabled = false
boolean retryEnabled = false
String resumeText = "Retry"
Downloader downloader = model.downloads[selected].downloader
switch(downloader.currentState) {
case Downloader.DownloadState.DOWNLOADING:
case Downloader.DownloadState.HASHLIST:
case Downloader.DownloadState.CONNECTING:
pauseEnabled = true
cancelEnabled = true
retryEnabled = false
break
case Downloader.DownloadState.FAILED:
pauseEnabled = false
cancelEnabled = true
retryEnabled = true
break
case Downloader.DownloadState.PAUSED:
pauseEnabled = false
cancelEnabled = true
retryEnabled = true
resumeText = "Resume"
break
default :
pauseEnabled = false
cancelEnabled = false
retryEnabled = false
}
@ -429,6 +473,12 @@ class MainFrameView {
})
menu.add(copyHashToClipboard)
if (pauseEnabled) {
JMenuItem pause = new JMenuItem("Pause")
pause.addActionListener({mvcGroup.controller.pause()})
menu.add(pause)
}
if (cancelEnabled) {
JMenuItem cancel = new JMenuItem("Cancel")
cancel.addActionListener({mvcGroup.controller.cancel()})
@ -436,7 +486,7 @@ class MainFrameView {
}
if (retryEnabled) {
JMenuItem retry = new JMenuItem("Retry")
JMenuItem retry = new JMenuItem(resumeText)
retry.addActionListener({mvcGroup.controller.resume()})
menu.add(retry)
}
@ -466,11 +516,26 @@ class MainFrameView {
def shareFiles = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share")
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) {
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))
label(text : "Font", constraints : gbc(gridx: 0, 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))
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
// label(text : "Show Monitor", constraints : gbc(gridx :0, 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))
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, 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))
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))
showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, 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))
}
buttonsPanel = builder.panel {
gridBagLayout()

View File

@ -47,8 +47,9 @@ class SearchTabView {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) {
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: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.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: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination).toString()
@ -66,7 +67,13 @@ class SearchTabView {
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
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)
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() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
showPopupMenu(e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download()
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3)
showPopupMenu(menu, e)
showPopupMenu(e)
}
})
}
@ -131,12 +131,21 @@ class SearchTabView {
def closeTab = {
int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index)
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.parentGroup.model.trustButtonsEnabled = false
mvcGroup.parentGroup.model.downloadActionEnabled = false
mvcGroup.destroy()
}
def showPopupMenu(JPopupMenu menu, MouseEvent e) {
println "showing popup menu"
def showPopupMenu(MouseEvent e) {
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())
}

View File

@ -12,12 +12,12 @@ class UISettings {
UISettings(Properties props) {
lnf = props.getProperty("lnf", "system")
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "false"))
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"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false"))
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true"))
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
}
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,64 @@
############################################################
# 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 = 150000000
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
net.i2p.client.streaming.impl.level = FINE
net.i2p.client.impl.level = FINE