Compare commits
223 Commits
muwire-0.6
...
iconfix-re
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fdb64f5539 | ||
![]() |
5b4f3202d6 | ||
![]() |
eb1f2fe19d | ||
![]() |
17c59102ad | ||
![]() |
26e8300d18 | ||
![]() |
47ac0fd9ac | ||
![]() |
0b8b489169 | ||
![]() |
fb32690c7c | ||
![]() |
a11c504271 | ||
![]() |
76e726b520 | ||
![]() |
4f626615d8 | ||
![]() |
061a1a88dd | ||
![]() |
ad20d7cf9a | ||
![]() |
895df6cf94 | ||
![]() |
59b5d88829 | ||
![]() |
f382d2ecbf | ||
![]() |
6740d09479 | ||
![]() |
8cbada110e | ||
![]() |
33982dd24b | ||
![]() |
274edcc599 | ||
![]() |
af218a369c | ||
![]() |
f0aaa83b7f | ||
![]() |
b9c34cb944 | ||
![]() |
59353a6718 | ||
![]() |
c25546e1e1 | ||
![]() |
f9fb9e4f07 | ||
![]() |
72f2b2bd37 | ||
![]() |
eb242b0889 | ||
![]() |
6508522c9c | ||
![]() |
f38b8217c2 | ||
![]() |
c9c5e8617a | ||
![]() |
8c4bafda82 | ||
![]() |
c2044044c0 | ||
![]() |
cb54b30967 | ||
![]() |
c041f6baaa | ||
![]() |
bf28278f72 | ||
![]() |
6462675091 | ||
![]() |
5adf8d8276 | ||
![]() |
2fbab55f68 | ||
![]() |
0d783a6bcd | ||
![]() |
017454c4b3 | ||
![]() |
ec41985d31 | ||
![]() |
5daad35ee2 | ||
![]() |
8df9f63bc7 | ||
![]() |
367a43825f | ||
![]() |
7b34b0cffc | ||
![]() |
bb6692c38e | ||
![]() |
f1a2b103a8 | ||
![]() |
c1324c92ba | ||
![]() |
179c3438cd | ||
![]() |
7fa6812ee9 | ||
![]() |
a1c714b46e | ||
![]() |
4f7cf4fbfc | ||
![]() |
2d3e843d64 | ||
![]() |
2e36812740 | ||
![]() |
61340f346a | ||
![]() |
992daa1e45 | ||
![]() |
3b825263a7 | ||
![]() |
e1bf6c0821 | ||
![]() |
a6eca11479 | ||
![]() |
11aa6dda70 | ||
![]() |
3116e20c7c | ||
![]() |
58a92e7442 | ||
![]() |
d18cdb15cd | ||
![]() |
ed02b718d9 | ||
![]() |
564db3473c | ||
![]() |
6d6063829a | ||
![]() |
ecaec1df3b | ||
![]() |
8b99f83db8 | ||
![]() |
33b159477a | ||
![]() |
91d8175cc5 | ||
![]() |
b4c6c77167 | ||
![]() |
fb59d1ca0c | ||
![]() |
3de4c65d2f | ||
![]() |
91ea2c0184 | ||
![]() |
4a81a3539e | ||
fcfb506787 | |||
![]() |
44dc7b808f | ||
![]() |
339f4aaa3e | ||
![]() |
bf06c3b15f | ||
![]() |
b5e41d72b8 | ||
![]() |
2fe9309519 | ||
![]() |
2410ed7199 | ||
![]() |
9167c9edf7 | ||
![]() |
028a8d5044 | ||
![]() |
356d7fe2ff | ||
![]() |
9da7a90653 | ||
![]() |
2001419f1a | ||
![]() |
eec9bab081 | ||
![]() |
0a66267264 | ||
![]() |
ad698cf1b9 | ||
![]() |
fd9866c519 | ||
![]() |
83bea0c823 | ||
![]() |
71789d96d2 | ||
![]() |
7860aa2b1c | ||
![]() |
301c2ec0e2 | ||
![]() |
c306864781 | ||
![]() |
acee9a5805 | ||
![]() |
d34c4e1990 | ||
![]() |
7be3821e53 | ||
![]() |
872e932629 | ||
![]() |
84c7da1fe0 | ||
![]() |
4aed958319 | ||
![]() |
5fc0283da7 | ||
![]() |
c4d908f571 | ||
![]() |
4d5497c12f | ||
![]() |
1d22abfa88 | ||
![]() |
7a7ebc9690 | ||
![]() |
16d3a109ca | ||
![]() |
7864eebb24 | ||
![]() |
9f7aaec991 | ||
![]() |
1c214ad68a | ||
![]() |
3436af75bf | ||
![]() |
9b6a2fd952 | ||
![]() |
85ad3109f9 | ||
![]() |
293ff76ae9 | ||
![]() |
acb70f72d6 | ||
![]() |
62bb4f9e5f | ||
![]() |
03d6fb15f2 | ||
![]() |
699f3ce1b6 | ||
![]() |
7f9c8bddb6 | ||
![]() |
d111983d68 | ||
![]() |
50148e5603 | ||
![]() |
1054fe0935 | ||
![]() |
2de2badb0b | ||
![]() |
424922f2e3 | ||
![]() |
adce4b1574 | ||
![]() |
355535e660 | ||
![]() |
09db68182c | ||
![]() |
1e67139e74 | ||
![]() |
9837e1e3d7 | ||
![]() |
2c52486476 | ||
![]() |
a88dc17064 | ||
![]() |
862967bf8e | ||
![]() |
9f1f718870 | ||
![]() |
2fd0a3833f | ||
![]() |
435170cb1b | ||
![]() |
1c5fec7e9a | ||
![]() |
e2a0a37abf | ||
![]() |
a4bee73b8a | ||
![]() |
056e5800c2 | ||
![]() |
6e0d51c221 | ||
![]() |
496e2e7f91 | ||
![]() |
a560b14d91 | ||
![]() |
faad6b6b0e | ||
![]() |
dfc62b943f | ||
![]() |
244ce43794 | ||
![]() |
f0c8c11094 | ||
![]() |
11e320ef53 | ||
![]() |
aae88e80ee | ||
![]() |
bbf97311d1 | ||
![]() |
23b6995bf2 | ||
![]() |
518bdc44e6 | ||
![]() |
5368dbe181 | ||
![]() |
e216678d9a | ||
![]() |
4582cfa0b5 | ||
![]() |
5ea64ecb90 | ||
![]() |
bd9315954a | ||
![]() |
83bdf76c08 | ||
![]() |
a2ed308cd0 | ||
![]() |
4020df0a77 | ||
![]() |
6f4b4a2c2d | ||
![]() |
83cd5e57a2 | ||
![]() |
bb69535874 | ||
![]() |
b7033e3277 | ||
![]() |
4a9cea7d2e | ||
![]() |
2aea965d72 | ||
![]() |
9a6a1c8371 | ||
![]() |
2042bfccb7 | ||
![]() |
0d4b0df19d | ||
![]() |
f363296ed1 | ||
![]() |
8b33a5a284 | ||
![]() |
7e70dbda86 | ||
![]() |
c23db1293f | ||
![]() |
54f4874ad6 | ||
![]() |
886effa3b6 | ||
![]() |
64d8b98ee2 | ||
![]() |
2f2f620ae5 | ||
![]() |
9a74cc5026 | ||
![]() |
e3c5fe291d | ||
![]() |
c77b848d44 | ||
![]() |
cf5b5b164d | ||
![]() |
3a340e40c8 | ||
![]() |
e9eafe9380 | ||
![]() |
270a8519b4 | ||
![]() |
f8bbeb8ac0 | ||
![]() |
2a4db868aa | ||
![]() |
59219da1a2 | ||
![]() |
a5fb824f71 | ||
![]() |
68bc0bbf30 | ||
![]() |
c6c1ac1d93 | ||
![]() |
9646eadcb1 | ||
![]() |
db91c9171d | ||
![]() |
e542a50260 | ||
![]() |
a9539c5999 | ||
![]() |
d93dbbeb8b | ||
![]() |
45659f0dca | ||
![]() |
31a607ed7d | ||
![]() |
7a6538beff | ||
![]() |
509b5c3b99 | ||
![]() |
fbb710cfc8 | ||
![]() |
244015465a | ||
![]() |
7285c12b97 | ||
![]() |
aac259c0fe | ||
![]() |
e3f58f8f5a | ||
![]() |
045859fe04 | ||
![]() |
3a8c66e857 | ||
![]() |
773513b257 | ||
![]() |
83fe2e9b75 | ||
![]() |
455b0ea48e | ||
![]() |
f4c96db841 | ||
![]() |
fca8870283 | ||
![]() |
3efb04d7bb | ||
![]() |
62ce8ffa46 | ||
![]() |
05b70a4573 | ||
![]() |
b339784826 | ||
![]() |
488f2964ee | ||
![]() |
369779ab6a | ||
![]() |
f5fe3da09d | ||
![]() |
392deee34c | ||
![]() |
7183f15c5c | ||
![]() |
ca33535630 | ||
![]() |
54abf82a91 |
4
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
**/.settings
|
||||
**/build
|
||||
.gradle
|
||||
.project
|
||||
.classpath
|
||||
**/.project
|
||||
**/.classpath
|
||||
**/*.rej
|
||||
**/*.orig
|
||||
|
16
README.md
@@ -1,8 +1,10 @@
|
||||
The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and issues at http://git.idk.i2p/zlatinb/muwire
|
||||
|
||||
# MuWire - Easy Anonymous File-Sharing
|
||||
|
||||
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
|
||||
|
||||
The current stable release - 0.6.8 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
|
||||
The current stable release - 0.7.1 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
|
||||
|
||||
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
|
||||
|
||||
@@ -28,9 +30,7 @@ Type
|
||||
./gradlew gui:run
|
||||
```
|
||||
|
||||
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
|
||||
|
||||
[Default I2CP port]\: `7654`
|
||||
The setup wizard will ask you for the host and port of an I2P or I2Pd router.
|
||||
|
||||
## Running the CLI
|
||||
|
||||
@@ -49,6 +49,12 @@ MuWire is available as a Docker image. For more information see the [Docker] pa
|
||||
## Translations
|
||||
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
|
||||
|
||||
## Related Projects
|
||||
### MuWire Tracker Daemon
|
||||
The MuWire Tracker Daemon (or mwtrackerd for short) is a project to bring functionality similar to BitTorrent tracking to MuWire. For more info see the [Tracker] page.
|
||||
### MuCats
|
||||
MuCats is a project to create a website for hosting hashes of files shared on the MuWire network. For more info see the [MuCats] project.
|
||||
|
||||
## GPG Fingerprint
|
||||
|
||||
```
|
||||
@@ -67,3 +73,5 @@ You can find the full key at https://keybase.io/zlatinb
|
||||
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
|
||||
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
|
||||
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
|
||||
[Tracker]: https://github.com/zlatinb/muwire/wiki/Tracker-Daemon
|
||||
[MuCats]: https://github.com/zlatinb/mucats
|
||||
|
5
TODO.md
@@ -19,7 +19,8 @@ This helps with scalability
|
||||
* Enum i18n
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* Public Feed feature
|
||||
* Advertise file feed and browseability in upload headers
|
||||
* Manual polling / shared folder re-scan (because polling NAS doesn't work)
|
||||
|
||||
### Chat
|
||||
* echo "unknown/innappropriate command" in the console
|
||||
@@ -32,6 +33,8 @@ This helps with scalability
|
||||
### Swing GUI
|
||||
* I2P Status panel - display message when connected to external router
|
||||
* Search box - left identation
|
||||
* Ability to disable switching of tabs on actions
|
||||
* Ability to trust/browse/subscribe from uploads tab
|
||||
|
||||
### Web UI/Plugin
|
||||
* HTML 5 media players
|
||||
|
21
build.gradle
@@ -2,13 +2,26 @@ subprojects {
|
||||
apply plugin: 'groovy'
|
||||
|
||||
dependencies {
|
||||
compile 'org.codehaus.groovy:groovy:2.4.15'
|
||||
compile 'org.codehaus.groovy:groovy-jsr223:2.4.15'
|
||||
compile 'org.codehaus.groovy:groovy-json:2.4.15'
|
||||
compile "org.codehaus.groovy:groovy:${groovyVersion}"
|
||||
compile "org.codehaus.groovy:groovy-jsr223:${groovyVersion}"
|
||||
compile "org.codehaus.groovy:groovy-json:${groovyVersion}"
|
||||
}
|
||||
|
||||
compileGroovy {
|
||||
groovyOptions.optimizationOptions.indy = true
|
||||
groovyOptions.optimizationOptions.indy = false
|
||||
sourceCompatibility = project.sourceCompatibility
|
||||
targetCompatibility = project.targetCompatibility
|
||||
options.compilerArgs += project.compilerArgs
|
||||
options.deprecation = true
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
sourceCompatibility = project.sourceCompatibility
|
||||
targetCompatibility = project.targetCompatibility
|
||||
options.compilerArgs += project.compilerArgs
|
||||
options.deprecation = true
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.6.11"
|
||||
private static final String MW_VERSION = "0.7.1"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -28,7 +28,6 @@ class FilesModel {
|
||||
core.eventBus.register(FileLoadedEvent.class, this)
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
core.eventBus.register(FileHashedEvent.class, this)
|
||||
core.eventBus.register(AllFilesLoadedEvent.class, this)
|
||||
|
||||
Runnable refreshModel = {refreshModel()}
|
||||
Timer timer = new Timer(true)
|
||||
@@ -38,15 +37,6 @@ class FilesModel {
|
||||
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
def eventBus = core.eventBus
|
||||
guiThread.invokeLater {
|
||||
core.muOptions.watchedDirectories.each {
|
||||
eventBus.publish(new FileSharedEvent(file: new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
guiThread.invokeLater {
|
||||
sharedFiles.add(e.loadedFile)
|
||||
|
@@ -1,12 +1,38 @@
|
||||
apply plugin : 'application'
|
||||
mainClassName = 'com.muwire.core.Core'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
|
||||
dependencies {
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
compile "net.i2p:router:${i2pVersion}"
|
||||
compile "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
compile "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "net.i2p:i2p:${i2pVersion}"
|
||||
api "net.i2p:router:${i2pVersion}"
|
||||
api "net.i2p.client:mstreaming:${i2pVersion}"
|
||||
implementation "net.i2p.client:streaming:${i2pVersion}"
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.codehaus.groovy:groovy-all:3.0.4'
|
||||
}
|
||||
|
||||
|
||||
// this is necessary because applying both groovy and java-library doesn't work well
|
||||
configurations {
|
||||
apiElements.outgoing.variants {
|
||||
classes {
|
||||
artifact file: compileGroovy.destinationDir, builtBy: compileGroovy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish core to local maven repo for sister projects
|
||||
publishing {
|
||||
publications {
|
||||
muCore(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ import com.muwire.core.files.PersisterFolderService
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.ZipException
|
||||
|
||||
import com.muwire.core.chat.ChatDisconnectionEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
@@ -53,7 +55,11 @@ import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
|
||||
import com.muwire.core.files.directories.UISyncDirectoryEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConverter
|
||||
import com.muwire.core.files.directories.WatchedDirectoryManager
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@@ -79,6 +85,7 @@ import com.muwire.core.upload.UploadManager
|
||||
import com.muwire.core.util.MuWireLogManager
|
||||
import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.content.ContentManager
|
||||
import com.muwire.core.tracker.TrackerResponder
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.I2PAppContext
|
||||
@@ -106,19 +113,20 @@ public class Core {
|
||||
final Properties i2pOptions
|
||||
final MuWireSettings muOptions
|
||||
|
||||
private final I2PSession i2pSession;
|
||||
final I2PSession i2pSession;
|
||||
private I2PSocketManager i2pSocketManager
|
||||
final TrustService trustService
|
||||
final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final PersisterFolderService persisterFolderService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
final HostCache hostCache
|
||||
final ConnectionManager connectionManager
|
||||
private final CacheClient cacheClient
|
||||
private final UpdateClient updateClient
|
||||
private final ConnectionAcceptor connectionAcceptor
|
||||
final ConnectionAcceptor connectionAcceptor
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
private final DownloadManager downloadManager
|
||||
final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
@@ -128,6 +136,9 @@ public class Core {
|
||||
final ChatManager chatManager
|
||||
final FeedManager feedManager
|
||||
private final FeedClient feedClient
|
||||
private final WatchedDirectoryConverter watchedDirectoryConverter
|
||||
final WatchedDirectoryManager watchedDirectoryManager
|
||||
private final TrackerResponder trackerResponder
|
||||
|
||||
private final Router router
|
||||
|
||||
@@ -144,7 +155,11 @@ public class Core {
|
||||
// Read defaults
|
||||
def defaultI2PFile = getClass()
|
||||
.getClassLoader().getResource("defaults/i2p.properties");
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
try {
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
} catch (ZipException mystery) {
|
||||
log.log(Level.SEVERE, "couldn't load default i2p properties", mystery)
|
||||
}
|
||||
|
||||
def i2pOptionsFile = new File(home, "i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
@@ -155,15 +170,17 @@ public class Core {
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
}
|
||||
if (!(i2pOptions.hasProperty("i2np.ntcp.port")
|
||||
&& i2pOptions.hasProperty("i2np.udp.port")
|
||||
if (!(i2pOptions.containsKey("i2np.ntcp.port")
|
||||
&& i2pOptions.containsKey("i2np.udp.port")
|
||||
)) {
|
||||
Random r = new Random()
|
||||
int port = r.nextInt(60000) + 4000
|
||||
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
|
||||
i2pOptions["i2np.ntcp.port"] = String.valueOf(port)
|
||||
i2pOptions["i2np.udp.port"] = String.valueOf(port)
|
||||
i2pOptionsFile.withOutputStream { i2pOptions.store(it, "") }
|
||||
}
|
||||
|
||||
i2pOptions['i2cp.leaseSetEncType']='4,0'
|
||||
|
||||
if (!props.embeddedRouter) {
|
||||
if (!(I2PAppContext.getGlobalContext() instanceof RouterContext)) {
|
||||
@@ -204,14 +221,13 @@ public class Core {
|
||||
|
||||
|
||||
// options like tunnel length and quantity
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
i2pSocketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
socketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = socketManager.getSession()
|
||||
i2pSocketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
i2pSocketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
i2pSocketManager.addDisconnectListener({eventBus.publish(new RouterDisconnectedEvent())} as DisconnectListener)
|
||||
i2pSession = i2pSocketManager.getSession()
|
||||
|
||||
def destination = new Destination()
|
||||
spk = new SigningPrivateKey(Constants.SIG_TYPE)
|
||||
@@ -311,7 +327,7 @@ public class Core {
|
||||
log.info("running as plugin, not initializing update client")
|
||||
|
||||
log.info("initializing connector")
|
||||
I2PConnector i2pConnector = new I2PConnector(socketManager)
|
||||
I2PConnector i2pConnector = new I2PConnector(i2pSocketManager)
|
||||
|
||||
log.info("initializing certificate client")
|
||||
CertificateClient certificateClient = new CertificateClient(eventBus, i2pConnector)
|
||||
@@ -359,6 +375,9 @@ public class Core {
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
|
||||
|
||||
log.info("initializing tracker responder")
|
||||
trackerResponder = new TrackerResponder(i2pSession, props, fileManager, downloadManager, meshManager, trustService, me)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
@@ -374,16 +393,11 @@ public class Core {
|
||||
}
|
||||
|
||||
log.info("initializing acceptor")
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(socketManager)
|
||||
I2PAcceptor i2pAcceptor = new I2PAcceptor(i2pSocketManager)
|
||||
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
|
||||
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, fileManager, connectionEstablisher,
|
||||
certificateManager, chatServer)
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, props)
|
||||
eventBus.register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
eventBus.register(AllFilesLoadedEvent.class, directoryWatcher)
|
||||
eventBus.register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
|
||||
log.info("initializing hasher service")
|
||||
hasherService = new HasherService(new FileHasher(), eventBus, fileManager, props)
|
||||
@@ -405,6 +419,28 @@ public class Core {
|
||||
BrowseManager browseManager = new BrowseManager(i2pConnector, eventBus, me)
|
||||
eventBus.register(UIBrowseEvent.class, browseManager)
|
||||
|
||||
log.info("initializing watched directory converter")
|
||||
watchedDirectoryConverter = new WatchedDirectoryConverter(this)
|
||||
eventBus.register(AllFilesLoadedEvent.class, watchedDirectoryConverter)
|
||||
|
||||
log.info("initializing watched directory manager")
|
||||
watchedDirectoryManager = new WatchedDirectoryManager(home, eventBus, fileManager)
|
||||
eventBus.with {
|
||||
register(WatchedDirectoryConfigurationEvent.class, watchedDirectoryManager)
|
||||
register(WatchedDirectoryConvertedEvent.class, watchedDirectoryManager)
|
||||
register(FileSharedEvent.class, watchedDirectoryManager)
|
||||
register(DirectoryUnsharedEvent.class, watchedDirectoryManager)
|
||||
register(UISyncDirectoryEvent.class, watchedDirectoryManager)
|
||||
}
|
||||
|
||||
log.info("initializing directory watcher")
|
||||
directoryWatcher = new DirectoryWatcher(eventBus, fileManager, home, watchedDirectoryManager)
|
||||
eventBus.with {
|
||||
register(DirectoryWatchedEvent.class, directoryWatcher)
|
||||
register(WatchedDirectoryConvertedEvent.class, directoryWatcher)
|
||||
register(DirectoryUnsharedEvent.class, directoryWatcher)
|
||||
register(WatchedDirectoryConfigurationEvent.class, directoryWatcher)
|
||||
}
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
@@ -421,6 +457,7 @@ public class Core {
|
||||
updateClient?.start()
|
||||
feedManager.start()
|
||||
feedClient.start()
|
||||
trackerResponder.start()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@@ -448,6 +485,8 @@ public class Core {
|
||||
connectionEstablisher.stop()
|
||||
log.info("shutting down directory watcher")
|
||||
directoryWatcher.stop()
|
||||
log.info("shutting down watch directory manager")
|
||||
watchedDirectoryManager.shutdown()
|
||||
log.info("shutting down cache client")
|
||||
cacheClient.stop()
|
||||
log.info("shutting down chat server")
|
||||
@@ -458,10 +497,16 @@ public class Core {
|
||||
feedManager.stop()
|
||||
log.info("shutting down feed client")
|
||||
feedClient.stop()
|
||||
log.info("shutting down tracker responder")
|
||||
trackerResponder.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
log.info("killing i2p session")
|
||||
i2pSession.destroySession()
|
||||
if (updateClient != null) {
|
||||
log.info("shutting down update client")
|
||||
updateClient.stop()
|
||||
}
|
||||
log.info("killing socket manager")
|
||||
i2pSocketManager.destroySocketManager()
|
||||
if (router != null) {
|
||||
log.info("shutting down embedded router")
|
||||
router.shutdown(0)
|
||||
@@ -505,7 +550,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.6.11")
|
||||
Core core = new Core(props, home, "0.7.1")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -31,6 +31,7 @@ class MuWireSettings {
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
boolean allowTracking
|
||||
|
||||
boolean fileFeed
|
||||
boolean advertiseFeed
|
||||
@@ -92,6 +93,7 @@ class MuWireSettings {
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
allowTracking = Boolean.valueOf(props.getProperty("allowTracking","true"))
|
||||
|
||||
// feed settings
|
||||
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
|
||||
@@ -100,9 +102,9 @@ class MuWireSettings {
|
||||
defaultFeedAutoDownload = Boolean.valueOf(props.getProperty("defaultFeedAutoDownload", "false"))
|
||||
defaultFeedItemsToKeep = Integer.valueOf(props.getProperty("defaultFeedItemsToKeep", "1000"))
|
||||
defaultFeedSequential = Boolean.valueOf(props.getProperty("defaultFeedSequential", "false"))
|
||||
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60"))
|
||||
defaultFeedUpdateInterval = Integer.valueOf(props.getProperty("defaultFeedUpdateInterval", "60000"))
|
||||
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","10"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
startChatServer = Boolean.valueOf(props.getProperty("startChatServer","false"))
|
||||
@@ -157,6 +159,7 @@ class MuWireSettings {
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
props.setProperty("allowTracking", String.valueOf(allowTracking))
|
||||
|
||||
// feed settings
|
||||
props.setProperty("fileFeed", String.valueOf(fileFeed))
|
||||
|
@@ -233,7 +233,7 @@ class ChatConnection implements ChatLink {
|
||||
daos.close()
|
||||
byte [] signed = baos.toByteArray()
|
||||
def spk = sender.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
def signature = new Signature(spk.getType(), sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, signed, spk)
|
||||
}
|
||||
|
||||
|
@@ -244,7 +244,7 @@ abstract class Connection implements Closeable {
|
||||
else
|
||||
payload = String.join(" ",search.keywords).getBytes(StandardCharsets.UTF_8)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
def signature = new Signature(spk.getType(), sig)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("signature didn't match keywords")
|
||||
return
|
||||
@@ -266,7 +266,7 @@ abstract class Connection implements Closeable {
|
||||
queryTime = search.queryTime
|
||||
byte [] payload = (search.uuid + String.valueOf(queryTime)).getBytes(StandardCharsets.US_ASCII)
|
||||
def spk = originator.destination.getSigningPublicKey()
|
||||
def signature = new Signature(Constants.SIG_TYPE, sig2)
|
||||
def signature = new Signature(spk.getType(), sig2)
|
||||
if (!DSAEngine.getInstance().verifySignature(signature, payload, spk)) {
|
||||
log.info("extended signature didn't match uuid and timestamp")
|
||||
return
|
||||
|
@@ -60,7 +60,7 @@ class ConnectionAcceptor {
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
private volatile int browsed
|
||||
volatile int browsed
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
@@ -574,7 +574,9 @@ class ConnectionAcceptor {
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
final long now = System.currentTimeMillis();
|
||||
published.each {
|
||||
it.hit(requestor, now, "Feed Update");
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = FeedItems.sharedFileToObj(it, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
|
@@ -34,6 +34,8 @@ class ConnectionEstablisher {
|
||||
final ExecutorService executor, closer
|
||||
|
||||
final Set inProgress = new ConcurrentHashSet()
|
||||
|
||||
private volatile boolean shutdown
|
||||
|
||||
ConnectionEstablisher(){}
|
||||
|
||||
@@ -60,12 +62,15 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
timer.cancel()
|
||||
executor.shutdownNow()
|
||||
closer.shutdownNow()
|
||||
}
|
||||
|
||||
private void connectIfNeeded() {
|
||||
if (shutdown)
|
||||
return
|
||||
if (!connectionManager.needsConnections())
|
||||
return
|
||||
if (inProgress.size() >= CONCURRENT)
|
||||
@@ -89,6 +94,8 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void connect(Destination toTry) {
|
||||
if (shutdown)
|
||||
return
|
||||
log.info("starting connect to ${toTry.toBase32()}")
|
||||
try {
|
||||
def endpoint = i2pConnector.connect(toTry)
|
||||
@@ -123,6 +130,8 @@ class ConnectionEstablisher {
|
||||
}
|
||||
|
||||
private void fail(Endpoint endpoint) {
|
||||
if (shutdown)
|
||||
return
|
||||
if (!closer.isShutdown()) {
|
||||
closer.execute {
|
||||
endpoint.close()
|
||||
|
@@ -105,8 +105,8 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
@Override
|
||||
void shutdown() {
|
||||
super.shutdown()
|
||||
peerConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().parallel().forEach({v -> v.close()})
|
||||
peerConnections.values().stream().forEach({v -> v.close()})
|
||||
leafConnections.values().stream().forEach({v -> v.close()})
|
||||
peerConnections.clear()
|
||||
leafConnections.clear()
|
||||
}
|
||||
|
@@ -242,4 +242,8 @@ public class DownloadManager {
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
|
||||
public boolean isDownloading(InfoHash infoHash) {
|
||||
downloaders.containsKey(infoHash)
|
||||
}
|
||||
}
|
||||
|
@@ -183,15 +183,14 @@ class DownloadSession {
|
||||
mapped.position(position)
|
||||
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
DataInputStream dis = new DataInputStream(is)
|
||||
while(mapped.hasRemaining()) {
|
||||
if (mapped.remaining() < tmp.length)
|
||||
tmp = new byte[mapped.remaining()]
|
||||
int read = is.read(tmp)
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
dis.readFully(tmp)
|
||||
synchronized(this) {
|
||||
mapped.put(tmp, 0, read)
|
||||
dataSinceLastRead.addAndGet(read)
|
||||
mapped.put(tmp)
|
||||
dataSinceLastRead.addAndGet(tmp.length)
|
||||
pieces.markPartial(piece, mapped.position())
|
||||
}
|
||||
}
|
||||
|
@@ -160,7 +160,7 @@ public class Downloader {
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
currSpeed = (int) (dataRead * 1000.0d / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
|
@@ -2,10 +2,11 @@ package com.muwire.core.download
|
||||
|
||||
class Pieces {
|
||||
private final BitSet done, claimed
|
||||
private final int nPieces
|
||||
final int nPieces
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
private int cachedDone;
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@@ -78,6 +79,7 @@ class Pieces {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
cachedDone = done.cardinality();
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
@@ -91,11 +93,11 @@ class Pieces {
|
||||
}
|
||||
|
||||
synchronized boolean isComplete() {
|
||||
done.cardinality() == nPieces
|
||||
cachedDone == nPieces
|
||||
}
|
||||
|
||||
synchronized int donePieces() {
|
||||
done.cardinality()
|
||||
cachedDone
|
||||
}
|
||||
|
||||
synchronized boolean isDownloaded(int piece) {
|
||||
@@ -104,6 +106,7 @@ class Pieces {
|
||||
|
||||
synchronized void clearAll() {
|
||||
done.clear()
|
||||
cachedDone = 0
|
||||
claimed.clear()
|
||||
partials.clear()
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@ class Certificate {
|
||||
|
||||
byte [] payload = baos.toByteArray()
|
||||
SigningPublicKey spk = issuer.destination.getSigningPublicKey()
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig)
|
||||
Signature signature = new Signature(spk.getType(), sig)
|
||||
DSAEngine.getInstance().verifySignature(signature, payload, spk)
|
||||
}
|
||||
|
||||
|
@@ -121,8 +121,13 @@ abstract class BasePersisterService extends Service{
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
if (it.searcher != null) {
|
||||
try {
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
} catch (Exception ignore) {
|
||||
return
|
||||
}
|
||||
}
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
|
@@ -4,8 +4,9 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
boolean deleted
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
super.toString() + " unshared directory "+ directory.toString() + " deleted $deleted"
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConvertedEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectoryManager
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.util.SystemVersion
|
||||
@@ -33,27 +36,27 @@ class DirectoryWatcher {
|
||||
}
|
||||
|
||||
private final File home
|
||||
private final MuWireSettings muOptions
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final WatchedDirectoryManager watchedDirectoryManager
|
||||
private final Thread watcherThread, publisherThread
|
||||
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
|
||||
private final Map<File, WatchKey> watchedDirectories = new ConcurrentHashMap<>()
|
||||
private WatchService watchService
|
||||
private volatile boolean shutdown
|
||||
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, MuWireSettings muOptions) {
|
||||
DirectoryWatcher(EventBus eventBus, FileManager fileManager, File home, WatchedDirectoryManager watchedDirectoryManager) {
|
||||
this.home = home
|
||||
this.muOptions = muOptions
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.watchedDirectoryManager = watchedDirectoryManager
|
||||
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 onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
|
||||
watchService = FileSystems.getDefault().newWatchService()
|
||||
watcherThread.start()
|
||||
publisherThread.start()
|
||||
@@ -71,26 +74,26 @@ class DirectoryWatcher {
|
||||
Path path = canonical.toPath()
|
||||
WatchKey wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(canonical, wk)
|
||||
|
||||
if (muOptions.watchedDirectories.add(canonical.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
|
||||
if (muOptions.watchedDirectories.remove(e.directory.toString()))
|
||||
saveMuSettings()
|
||||
}
|
||||
|
||||
private void saveMuSettings() {
|
||||
File muSettingsFile = new File(home, "MuWire.properties")
|
||||
muSettingsFile.withPrintWriter("UTF-8", {
|
||||
muOptions.write(it)
|
||||
})
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
if (watchService == null)
|
||||
return // still converting
|
||||
if (!e.autoWatch) {
|
||||
WatchKey wk = watchedDirectories.remove(e.directory)
|
||||
wk?.cancel()
|
||||
} else if (!watchedDirectories.containsKey(e.directory)) {
|
||||
Path path = e.directory.toPath()
|
||||
def wk = path.register(watchService, kinds)
|
||||
watchedDirectories.put(e.directory, wk)
|
||||
} // else it was already watched
|
||||
}
|
||||
|
||||
|
||||
private void watch() {
|
||||
try {
|
||||
while(!shutdown) {
|
||||
@@ -115,7 +118,7 @@ class DirectoryWatcher {
|
||||
File f= join(parent, path)
|
||||
log.fine("created entry $f")
|
||||
if (f.isDirectory())
|
||||
f.toPath().register(watchService, kinds)
|
||||
eventBus.publish(new FileSharedEvent(file : f, fromWatch : true))
|
||||
else
|
||||
waitingFiles.put(f, System.currentTimeMillis())
|
||||
}
|
||||
@@ -133,6 +136,10 @@ class DirectoryWatcher {
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
else if (watchedDirectoryManager.isWatched(f))
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
|
||||
else
|
||||
log.fine("Entry was not relevant");
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
@@ -149,7 +156,7 @@ class DirectoryWatcher {
|
||||
waitingFiles.each { file, timestamp ->
|
||||
if (now - timestamp > WAIT_TIME) {
|
||||
log.fine("publishing file $file")
|
||||
eventBus.publish new FileSharedEvent(file : file)
|
||||
eventBus.publish new FileSharedEvent(file : file, fromWatch: true)
|
||||
published << file
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ class FileManager {
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree<Void> negativeTree = new FileTree<>()
|
||||
final FileTree<SharedFile> positiveTree = new FileTree<>()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
@@ -87,6 +88,7 @@ class FileManager {
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
positiveTree.add(sf.file, sf);
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
@@ -130,6 +132,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
positiveTree.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file,null)
|
||||
saveNegativeTree()
|
||||
@@ -246,14 +249,26 @@ class FileManager {
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
SharedFile sf = fileToSharedFile.get(it)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
if (!e.deleted) {
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
SharedFile sf = fileToSharedFile.get(it)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def cb = new DirDeletionCallback()
|
||||
positiveTree.traverse(e.directory, cb)
|
||||
positiveTree.remove(e.directory)
|
||||
cb.unsharedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : it, deleted: true))
|
||||
}
|
||||
cb.subDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted : true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,4 +285,25 @@ class FileManager {
|
||||
collect(Collectors.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirDeletionCallback implements FileTreeCallback<SharedFile> {
|
||||
|
||||
final List<File> subDirs = new ArrayList<>()
|
||||
final List<SharedFile> unsharedFiles = new ArrayList<>()
|
||||
|
||||
@Override
|
||||
public void onDirectoryEnter(File file) {
|
||||
subDirs.add(file)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryLeave() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFile(File file, SharedFile value) {
|
||||
unsharedFiles << value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -5,9 +5,10 @@ import com.muwire.core.Event
|
||||
class FileSharedEvent extends Event {
|
||||
|
||||
File file
|
||||
boolean fromWatch
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString() + " file: "+file.getAbsolutePath()
|
||||
return super.toString() + " file: "+file.getAbsolutePath() + " fromWatch: $fromWatch"
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ class FileTree<T> {
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.isFile = element.isFile()
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
@@ -64,7 +65,7 @@ class FileTree<T> {
|
||||
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
|
||||
boolean leave = false
|
||||
if (node.file != null) {
|
||||
if (node.file.isFile())
|
||||
if (node.isFile)
|
||||
callback.onFile(node.file, node.value)
|
||||
else {
|
||||
leave = true
|
||||
@@ -88,7 +89,7 @@ class FileTree<T> {
|
||||
node = fileToNode.get(parent)
|
||||
|
||||
node.children.each {
|
||||
if (it.file.isFile())
|
||||
if (it.isFile)
|
||||
callback.onFile(it.file, it.value)
|
||||
else
|
||||
callback.onDirectory(it.file)
|
||||
@@ -98,6 +99,7 @@ class FileTree<T> {
|
||||
public static class TreeNode<T> {
|
||||
TreeNode parent
|
||||
File file
|
||||
boolean isFile
|
||||
T value;
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
|
@@ -53,7 +53,6 @@ class HasherService {
|
||||
|
||||
private void process(File f) {
|
||||
if (f.isDirectory()) {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : f))
|
||||
f.listFiles().each {
|
||||
eventBus.publish new FileSharedEvent(file: it)
|
||||
}
|
||||
|
@@ -116,7 +116,7 @@ class PersisterFolderService extends BasePersisterService {
|
||||
try {
|
||||
_load()
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files", e)
|
||||
}
|
||||
} else {
|
||||
|
@@ -62,7 +62,7 @@ class PersisterService extends BasePersisterService {
|
||||
new File(location.absolutePath + ".bak")
|
||||
)
|
||||
listener.publish(new PersisterDoneEvent())
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
} else {
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UISyncDirectoryEvent extends Event {
|
||||
File directory
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class WatchedDirectory {
|
||||
final File directory
|
||||
final String encodedName
|
||||
boolean autoWatch
|
||||
int syncInterval
|
||||
long lastSync
|
||||
|
||||
WatchedDirectory(File directory) {
|
||||
this.directory = directory.getCanonicalFile()
|
||||
this.encodedName = Base64.encode(DataUtil.encodei18nString(directory.getAbsolutePath()))
|
||||
}
|
||||
|
||||
def toJson() {
|
||||
def rv = [:]
|
||||
rv.directory = encodedName
|
||||
rv.autoWatch = autoWatch
|
||||
rv.syncInterval = syncInterval
|
||||
rv.lastSync = lastSync
|
||||
rv
|
||||
}
|
||||
|
||||
static WatchedDirectory fromJson(def json) {
|
||||
String dirName = DataUtil.readi18nString(Base64.decode(json.directory))
|
||||
File dir = new File(dirName)
|
||||
def rv = new WatchedDirectory(dir)
|
||||
rv.autoWatch = json.autoWatch
|
||||
rv.syncInterval = json.syncInterval
|
||||
rv.lastSync = json.lastSync
|
||||
rv
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class WatchedDirectoryConfigurationEvent extends Event {
|
||||
File directory
|
||||
boolean autoWatch
|
||||
int syncInterval
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Emitted when converting an old watched directory entry to the
|
||||
* new format.
|
||||
*/
|
||||
class WatchedDirectoryConvertedEvent extends Event {
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
/**
|
||||
* converts the setting-based format to new folder-based format.
|
||||
*/
|
||||
class WatchedDirectoryConverter {
|
||||
|
||||
private final Core core
|
||||
|
||||
WatchedDirectoryConverter(Core core) {
|
||||
this.core = core
|
||||
}
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
core.getMuOptions().getWatchedDirectories().each {
|
||||
File directory = new File(it)
|
||||
directory = directory.getCanonicalFile()
|
||||
core.eventBus.publish(new WatchedDirectoryConfigurationEvent(directory : directory, autoWatch: true))
|
||||
}
|
||||
core.getMuOptions().getWatchedDirectories().clear()
|
||||
core.saveMuSettings()
|
||||
core.eventBus.publish(new WatchedDirectoryConvertedEvent())
|
||||
}
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.stream.Stream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
import com.muwire.core.files.FileListCallback
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class WatchedDirectoryManager {
|
||||
|
||||
private final File home
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
|
||||
private final Map<File, WatchedDirectory> watchedDirs = new ConcurrentHashMap<>()
|
||||
|
||||
private final ExecutorService diskIO = Executors.newSingleThreadExecutor({r ->
|
||||
Thread t = new Thread(r, "disk-io")
|
||||
t.setDaemon(true)
|
||||
t
|
||||
} as ThreadFactory)
|
||||
|
||||
private final Timer timer = new Timer("directory-timer", true)
|
||||
|
||||
private boolean converting = true
|
||||
|
||||
WatchedDirectoryManager(File home, EventBus eventBus, FileManager fileManager) {
|
||||
this.home = new File(home, "directories")
|
||||
this.home.mkdir()
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
}
|
||||
|
||||
public boolean isWatched(File f) {
|
||||
watchedDirs.containsKey(f)
|
||||
}
|
||||
|
||||
public Stream<WatchedDirectory> getWatchedDirsStream() {
|
||||
watchedDirs.values().stream()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
diskIO.shutdown()
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
void onUISyncDirectoryEvent(UISyncDirectoryEvent e) {
|
||||
def wd = watchedDirs.get(e.directory)
|
||||
if (wd == null) {
|
||||
log.warning("Got a sync event for non-watched dir ${e.directory}")
|
||||
return
|
||||
}
|
||||
diskIO.submit({sync(wd, System.currentTimeMillis())} as Runnable)
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
if (converting) {
|
||||
def newDir = new WatchedDirectory(e.directory)
|
||||
// conversion is always autowatch really
|
||||
newDir.autoWatch = e.autoWatch
|
||||
persist(newDir)
|
||||
} else {
|
||||
def wd = watchedDirs.get(e.directory)
|
||||
if (wd == null) {
|
||||
log.severe("got a configuration event for a non-watched directory ${e.directory}")
|
||||
return
|
||||
}
|
||||
wd.autoWatch = e.autoWatch
|
||||
wd.syncInterval = e.syncInterval
|
||||
persist(wd)
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConvertedEvent(WatchedDirectoryConvertedEvent e) {
|
||||
converting = false
|
||||
diskIO.submit({
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(home.toPath()).filter({
|
||||
it.getFileName().toString().endsWith(".json")
|
||||
}).
|
||||
forEach {
|
||||
def parsed = slurper.parse(it.toFile())
|
||||
WatchedDirectory wd = WatchedDirectory.fromJson(parsed)
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
}
|
||||
watchedDirs.values().stream().filter({it.autoWatch}).forEach {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : it.directory))
|
||||
eventBus.publish(new FileSharedEvent(file : it.directory))
|
||||
}
|
||||
timer.schedule({sync()} as TimerTask, 1000, 1000)
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private void persist(WatchedDirectory dir) {
|
||||
diskIO.submit({doPersist(dir)} as Runnable)
|
||||
}
|
||||
|
||||
private void doPersist(WatchedDirectory dir) {
|
||||
def json = JsonOutput.toJson(dir.toJson())
|
||||
def targetFile = new File(home, dir.getEncodedName() + ".json")
|
||||
targetFile.text = json
|
||||
}
|
||||
|
||||
void onFileSharedEvent(FileSharedEvent e) {
|
||||
if (e.file.isFile() || watchedDirs.containsKey(e.file))
|
||||
return
|
||||
|
||||
def wd = new WatchedDirectory(e.file)
|
||||
if (e.fromWatch) {
|
||||
// parent should be already watched, copy settings
|
||||
def parent = watchedDirs.get(e.file.getParentFile())
|
||||
if (parent == null) {
|
||||
log.severe("watching found a directory without a watched parent? ${e.file}")
|
||||
return
|
||||
}
|
||||
wd.autoWatch = parent.autoWatch
|
||||
wd.syncInterval = parent.syncInterval
|
||||
} else
|
||||
wd.autoWatch = true
|
||||
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
persist(wd)
|
||||
if (wd.autoWatch)
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory: wd.directory))
|
||||
}
|
||||
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
def wd = watchedDirs.remove(e.directory)
|
||||
if (wd == null) {
|
||||
log.warning("unshared a directory that wasn't watched? ${e.directory}")
|
||||
return
|
||||
}
|
||||
|
||||
File persistFile = new File(home, wd.getEncodedName() + ".json")
|
||||
persistFile.delete()
|
||||
}
|
||||
|
||||
private void sync() {
|
||||
long now = System.currentTimeMillis()
|
||||
watchedDirs.values().stream().
|
||||
filter({!it.autoWatch}).
|
||||
filter({it.syncInterval > 0}).
|
||||
filter({it.lastSync + it.syncInterval * 1000 < now}).
|
||||
forEach({wd -> diskIO.submit({sync(wd, now)} as Runnable )})
|
||||
}
|
||||
|
||||
private void sync(WatchedDirectory wd, long now) {
|
||||
log.fine("syncing ${wd.directory}")
|
||||
wd.lastSync = now
|
||||
doPersist(wd)
|
||||
eventBus.publish(new WatchedDirectorySyncEvent(directory: wd.directory, when: now))
|
||||
|
||||
def cb = new DirSyncCallback()
|
||||
fileManager.positiveTree.list(wd.directory, cb)
|
||||
|
||||
Set<File> filesOnFS = new HashSet<>()
|
||||
Set<File> dirsOnFS = new HashSet<>()
|
||||
wd.directory.listFiles().each {
|
||||
File canonical = it.getCanonicalFile()
|
||||
if (canonical.isFile())
|
||||
filesOnFS.add(canonical)
|
||||
else
|
||||
dirsOnFS.add(canonical)
|
||||
}
|
||||
|
||||
Set<File> addedFiles = new HashSet<>(filesOnFS)
|
||||
addedFiles.removeAll(cb.files)
|
||||
addedFiles.each {
|
||||
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
|
||||
}
|
||||
Set<File> addedDirs = new HashSet<>(dirsOnFS)
|
||||
addedDirs.removeAll(cb.dirs)
|
||||
addedDirs.each {
|
||||
eventBus.publish(new FileSharedEvent(file : it, fromWatch : true))
|
||||
}
|
||||
|
||||
Set<File> deletedFiles = new HashSet<>(cb.files)
|
||||
deletedFiles.removeAll(filesOnFS)
|
||||
deletedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : fileManager.getFileToSharedFile().get(it), deleted : true))
|
||||
}
|
||||
Set<File> deletedDirs = new HashSet<>(cb.dirs)
|
||||
deletedDirs.removeAll(dirsOnFS)
|
||||
deletedDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted: true))
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirSyncCallback implements FileListCallback<SharedFile> {
|
||||
|
||||
private final Set<File> files = new HashSet<>()
|
||||
private final Set<File> dirs = new HashSet<>()
|
||||
|
||||
@Override
|
||||
public void onFile(File f, SharedFile value) {
|
||||
files.add(f)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectory(File f) {
|
||||
dirs.add(f)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package com.muwire.core.files.directories
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class WatchedDirectorySyncEvent extends Event {
|
||||
File directory
|
||||
long when
|
||||
}
|
@@ -127,7 +127,8 @@ class CacheClient {
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe "I2P session disconnected"
|
||||
if (!stopped.get())
|
||||
log.severe "Cache client I2P session disconnected"
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -8,10 +8,8 @@ class CacheServers {
|
||||
private static Set<Destination> CACHES = [
|
||||
// zlatinb
|
||||
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
|
||||
// sNL
|
||||
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
|
||||
// dark_trion
|
||||
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
|
||||
// echelon
|
||||
new Destination("2MJTl8gYVPK43iJZJa~-5K1OchgPaPHXpqZmKIiKFvxyy8BlIJzUSrF4mazdta--shFHISfT0PEeI95j1yDyKMpGxatUyjSt3ZnyTfAehQR-H2kYV9FvjHo68uA9X5AaGYHKRYLuWMkihMXygd8ywoLjZtFP0UbKMPggfOZaWmjHF4081XoUXt~7MEAeYSQowndiUx0AH3HxNEiv0N373JJS61OsIXb5ctqVKkwIiX1R0ZxESzpP9Xwp8-T0ou8fsLksygbKyH~3K1CyTHjTS51Ux-U-CjOPH9rtCOjjAaifdyMpK0PxW1fVdoGswFywTz9Q-6DUMsIu5TsPMF0-UO1Wn8vCpVAWbBJAOtKCfBrGzp-E~GCbfCNs5xY19nLobMD5ehjsBdI1lXwGDCQ7kBOwC58uuC3BOoazgrB6IrGskyMTexawtthO9mhuPm91bq4xhNaCYHAe059xg5emnM7jFBVzQgjaZ5lOLn~HqcWofJ7oc0doE6XI6kOo~YncBQAEAAcAAA==")
|
||||
]
|
||||
|
||||
static List<Destination> getCacheServers() {
|
||||
|
@@ -10,7 +10,7 @@ import net.i2p.util.ConcurrentHashSet
|
||||
class Mesh {
|
||||
private final InfoHash infoHash
|
||||
private final Set<Persona> sources = new ConcurrentHashSet<>()
|
||||
private final Pieces pieces
|
||||
final Pieces pieces
|
||||
|
||||
Mesh(InfoHash infoHash, Pieces pieces) {
|
||||
this.infoHash = infoHash
|
||||
|
@@ -0,0 +1,218 @@
|
||||
package com.muwire.core.tracker
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.files.FileManager
|
||||
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.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.I2PSessionMuxedListener
|
||||
import net.i2p.client.SendMessageOptions
|
||||
import net.i2p.client.datagram.I2PDatagramDissector
|
||||
import net.i2p.client.datagram.I2PDatagramMaker
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class TrackerResponder {
|
||||
private final I2PSession i2pSession
|
||||
private final MuWireSettings muSettings
|
||||
private final FileManager fileManager
|
||||
private final DownloadManager downloadManager
|
||||
private final MeshManager meshManager
|
||||
private final TrustService trustService
|
||||
private final Persona me
|
||||
|
||||
private final Map<UUID,Long> uuids = new HashMap<>()
|
||||
private final Timer expireTimer = new Timer("tracker-responder-timer", true)
|
||||
|
||||
private static final long UUID_LIFETIME = 10 * 60 * 1000
|
||||
|
||||
private volatile boolean shutdown
|
||||
|
||||
TrackerResponder(I2PSession i2pSession, MuWireSettings muSettings,
|
||||
FileManager fileManager, DownloadManager downloadManager,
|
||||
MeshManager meshManager, TrustService trustService,
|
||||
Persona me) {
|
||||
this.i2pSession = i2pSession
|
||||
this.muSettings = muSettings
|
||||
this.fileManager = fileManager
|
||||
this.downloadManager = downloadManager
|
||||
this.meshManager = meshManager
|
||||
this.trustService = trustService
|
||||
this.me = me
|
||||
}
|
||||
|
||||
void start() {
|
||||
i2pSession.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT)
|
||||
expireTimer.schedule({expireUUIDs()} as TimerTask, UUID_LIFETIME, UUID_LIFETIME)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
expireTimer.cancel()
|
||||
}
|
||||
|
||||
private void expireUUIDs() {
|
||||
final long now = System.currentTimeMillis()
|
||||
synchronized(uuids) {
|
||||
for (Iterator<UUID> iter = uuids.keySet().iterator(); iter.hasNext();) {
|
||||
UUID uuid = iter.next();
|
||||
Long time = uuids.get(uuid)
|
||||
if (now - time > UUID_LIFETIME)
|
||||
iter.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void respond(host, json) {
|
||||
log.info("responding to host $host with json $json")
|
||||
|
||||
def message = JsonOutput.toJson(json)
|
||||
def maker = new I2PDatagramMaker(i2pSession)
|
||||
message = maker.makeI2PDatagram(message.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(false)
|
||||
i2pSession.sendMessage(host, message, 0, message.length, I2PSession.PROTO_DATAGRAM, Constants.TRACKER_PORT, Constants.TRACKER_PORT, options)
|
||||
}
|
||||
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
|
||||
if (proto != I2PSession.PROTO_DATAGRAM) {
|
||||
log.warning "Received unexpected protocol $proto"
|
||||
return
|
||||
}
|
||||
|
||||
byte[] payload = session.receiveMessage(msgId)
|
||||
def dissector = new I2PDatagramDissector()
|
||||
try {
|
||||
dissector.loadI2PDatagram(payload)
|
||||
def sender = dissector.getSender()
|
||||
|
||||
log.info("got a tracker datagram from ${sender.toBase32()}")
|
||||
|
||||
// if not trusted, just drop it
|
||||
TrustLevel trustLevel = trustService.getLevel(sender)
|
||||
|
||||
if (trustLevel == TrustLevel.DISTRUSTED ||
|
||||
(trustLevel == TrustLevel.NEUTRAL && !muSettings.allowUntrusted)) {
|
||||
log.info("dropping, untrusted")
|
||||
return
|
||||
}
|
||||
|
||||
payload = dissector.getPayload()
|
||||
def slurper = new JsonSlurper()
|
||||
def json = slurper.parse(payload)
|
||||
|
||||
if (json.type != "TrackerPing") {
|
||||
log.warning("unknown type $json.type")
|
||||
return
|
||||
}
|
||||
|
||||
def response = [:]
|
||||
response.type = "TrackerPong"
|
||||
response.me = me.toBase64()
|
||||
|
||||
if (json.infoHash == null) {
|
||||
log.warning("infoHash missing")
|
||||
return
|
||||
}
|
||||
|
||||
if (json.uuid == null) {
|
||||
log.warning("uuid missing")
|
||||
return
|
||||
}
|
||||
|
||||
UUID uuid = UUID.fromString(json.uuid)
|
||||
synchronized(uuids) {
|
||||
if (uuids.containsKey(uuid)) {
|
||||
log.warning("duplicate uuid $uuid")
|
||||
return
|
||||
}
|
||||
uuids.put(uuid, System.currentTimeMillis())
|
||||
}
|
||||
response.uuid = json.uuid
|
||||
|
||||
if (!muSettings.allowTracking) {
|
||||
response.code = 403
|
||||
respond(sender, response)
|
||||
return
|
||||
}
|
||||
|
||||
if (json.version != 1) {
|
||||
log.warning("unknown version $json.version")
|
||||
response.code = 400
|
||||
response.message = "I only support version 1"
|
||||
respond(sender,response)
|
||||
return
|
||||
}
|
||||
|
||||
byte[] infoHashBytes = Base64.decode(json.infoHash)
|
||||
InfoHash infoHash = new InfoHash(infoHashBytes)
|
||||
|
||||
log.info("servicing request for infoHash ${json.infoHash} with uuid ${json.uuid}")
|
||||
|
||||
if (!(fileManager.isShared(infoHash) || downloadManager.isDownloading(infoHash))) {
|
||||
response.code = 404
|
||||
respond(sender, response)
|
||||
return
|
||||
}
|
||||
|
||||
Mesh mesh = meshManager.get(infoHash)
|
||||
|
||||
if (fileManager.isShared(infoHash))
|
||||
response.code = 200
|
||||
else if (mesh != null) {
|
||||
response.code = 206
|
||||
Pieces pieces = mesh.getPieces()
|
||||
response.xHave = DataUtil.encodeXHave(pieces, pieces.getnPieces())
|
||||
}
|
||||
|
||||
if (mesh != null)
|
||||
response.altlocs = mesh.getRandom(10, me).stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||
|
||||
respond(sender,response)
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "invalid datagram", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAbuse(I2PSession session, int severity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
if (!shutdown)
|
||||
log.severe("Tracker Responder session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
public void errorOccurred(I2PSession session, String message, Throwable error) {
|
||||
log.log(Level.SEVERE, message, error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -2,6 +2,7 @@ package com.muwire.core.update
|
||||
|
||||
import java.util.logging.Level
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
@@ -48,6 +49,7 @@ class UpdateClient {
|
||||
private volatile boolean updateDownloading
|
||||
|
||||
private volatile String text
|
||||
private volatile boolean shutdown
|
||||
|
||||
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings,
|
||||
FileManager fileManager, Persona me, SigningPrivateKey spk) {
|
||||
@@ -63,11 +65,12 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
void start() {
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
|
||||
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT)
|
||||
timer.schedule({checkUpdate()} as TimerTask, 60000, 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ class UpdateClient {
|
||||
ping = maker.makeI2PDatagram(ping.bytes)
|
||||
def options = new SendMessageOptions()
|
||||
options.setSendLeaseSet(true)
|
||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
|
||||
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, Constants.UPDATE_PORT, 0, options)
|
||||
}
|
||||
|
||||
class Listener implements I2PSessionMuxedListener {
|
||||
@@ -198,7 +201,8 @@ class UpdateClient {
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe("I2P session disconnected")
|
||||
if (!shutdown)
|
||||
log.severe("I2P session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -106,7 +106,7 @@ class ContentUploader extends Uploader {
|
||||
return done ? 100 : 0
|
||||
int position = mapped.position()
|
||||
int total = request.getRange().end - request.getRange().start
|
||||
(int)(position * 100.0 / total)
|
||||
(int)(position * 100.0d / total)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -45,7 +45,7 @@ class HashListUploader extends Uploader {
|
||||
|
||||
@Override
|
||||
public synchronized int getProgress() {
|
||||
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||
(int)(mapped.position() * 100.0d / mapped.capacity())
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -22,7 +22,7 @@ class Request {
|
||||
|
||||
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
||||
Map<String, String> headers = parseHeaders(is)
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Range"))
|
||||
throw new IOException("Range header not found")
|
||||
@@ -60,7 +60,7 @@ class Request {
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
Map<String,String> headers = parseHeaders(is)
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
Persona downloader = null
|
||||
if (headers.containsKey("X-Persona")) {
|
||||
def encoded = headers["X-Persona"].trim()
|
||||
@@ -69,55 +69,4 @@ class Request {
|
||||
}
|
||||
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
|
||||
}
|
||||
|
||||
private static Map<String, String> parseHeaders(InputStream is) {
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||
while(headers.size() < Constants.MAX_HEADERS) {
|
||||
boolean r = false
|
||||
boolean n = false
|
||||
int idx = 0
|
||||
while (true) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException("Stream closed")
|
||||
|
||||
if (!r && read == N)
|
||||
throw new IOException("Received N before R")
|
||||
if (read == R) {
|
||||
if (r)
|
||||
throw new IOException("double R")
|
||||
r = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (r && !n) {
|
||||
if (read != N)
|
||||
throw new IOException("R not followed by N")
|
||||
n = true
|
||||
break
|
||||
}
|
||||
if (idx == 0x1 << 14)
|
||||
throw new IOException("Header too long")
|
||||
tmp[idx++] = read
|
||||
}
|
||||
|
||||
if (idx == 0)
|
||||
break
|
||||
|
||||
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
||||
log.fine("Read header $header")
|
||||
|
||||
int keyIdx = header.indexOf(":")
|
||||
if (keyIdx < 1)
|
||||
throw new IOException("Header key not found")
|
||||
if (keyIdx == header.length())
|
||||
throw new IOException("Header value not found")
|
||||
String key = header.substring(0, keyIdx)
|
||||
String value = header.substring(keyIdx + 1)
|
||||
headers.put(key, value)
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ abstract class Uploader {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0d / interval)
|
||||
dataSinceLastRead = 0
|
||||
|
||||
// normalize to speedArr.size
|
||||
|
@@ -4,6 +4,8 @@ import net.i2p.crypto.SigType;
|
||||
|
||||
public class Constants {
|
||||
public static final byte PERSONA_VERSION = (byte)1;
|
||||
public static final String INVALID_NICKNAME_CHARS = "'\"();<>=@$%";
|
||||
public static final int MAX_NICKNAME_LENGTH = 30;
|
||||
public static final byte FILE_CERT_VERSION = (byte)2;
|
||||
public static final int CHAT_VERSION = 1;
|
||||
|
||||
@@ -17,5 +19,8 @@ public class Constants {
|
||||
|
||||
public static final int MAX_COMMENT_LENGTH = 0x1 << 15;
|
||||
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
public static final long MAX_QUERY_AGE = 5 * 60 * 1000L;
|
||||
|
||||
public static final int UPDATE_PORT = 2;
|
||||
public static final int TRACKER_PORT = 3;
|
||||
}
|
||||
|
@@ -0,0 +1,25 @@
|
||||
package com.muwire.core;
|
||||
|
||||
public class InvalidNicknameException extends Exception {
|
||||
|
||||
public InvalidNicknameException() {
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public InvalidNicknameException(String message, Throwable cause, boolean enableSuppression,
|
||||
boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
}
|
@@ -7,6 +7,8 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.muwire.core.util.DataUtil;
|
||||
|
||||
import net.i2p.crypto.DSAEngine;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
@@ -25,12 +27,15 @@ public class Persona {
|
||||
private volatile String base64;
|
||||
private volatile byte[] payload;
|
||||
|
||||
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException {
|
||||
public Persona(InputStream personaStream) throws IOException, DataFormatException, InvalidSignatureException, InvalidNicknameException {
|
||||
version = (byte) (personaStream.read() & 0xFF);
|
||||
if (version != Constants.PERSONA_VERSION)
|
||||
throw new IOException("Unknown version "+version);
|
||||
|
||||
name = new Name(personaStream);
|
||||
if (!DataUtil.isValidName(name.name))
|
||||
throw new InvalidNicknameException(name.name + " is not a valid nickname");
|
||||
|
||||
destination = Destination.create(personaStream);
|
||||
sig = new byte[SIG_LEN];
|
||||
DataInputStream dis = new DataInputStream(personaStream);
|
||||
@@ -38,7 +43,7 @@ public class Persona {
|
||||
if (!verify(version, name, destination, sig))
|
||||
throw new InvalidSignatureException(getHumanReadableName() + " didn't verify");
|
||||
}
|
||||
|
||||
|
||||
private static boolean verify(byte version, Name name, Destination destination, byte [] sig)
|
||||
throws IOException, DataFormatException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
@@ -47,7 +52,7 @@ public class Persona {
|
||||
destination.writeBytes(baos);
|
||||
byte[] payload = baos.toByteArray();
|
||||
SigningPublicKey spk = destination.getSigningPublicKey();
|
||||
Signature signature = new Signature(Constants.SIG_TYPE, sig);
|
||||
Signature signature = new Signature(spk.getType(), sig);
|
||||
return DSAEngine.getInstance().verifySignature(signature, payload, spk);
|
||||
}
|
||||
|
||||
|
@@ -159,6 +159,18 @@ public class SharedFile {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Persona getSearcher() {
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
|
||||
}
|
||||
|
@@ -58,9 +58,9 @@ public class DataUtil {
|
||||
if (header.length != 3)
|
||||
throw new IllegalArgumentException("header length $header.length");
|
||||
|
||||
return (((int)(header[0] & 0x7F)) << 16) |
|
||||
(((int)(header[1] & 0xFF) << 8)) |
|
||||
((int)header[2] & 0xFF);
|
||||
return ((header[0] & 0x7F) << 16) |
|
||||
((header[1] & 0xFF) << 8) |
|
||||
(header[2] & 0xFF);
|
||||
}
|
||||
|
||||
public static String readi18nString(byte [] encoded) {
|
||||
@@ -174,7 +174,7 @@ public class DataUtil {
|
||||
clean.setAccessible(true);
|
||||
clean.invoke(cleaner.invoke(cb));
|
||||
} else {
|
||||
Class unsafeClass;
|
||||
Class<?> unsafeClass;
|
||||
try {
|
||||
unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
} catch(Exception ex) {
|
||||
@@ -216,4 +216,13 @@ public class DataUtil {
|
||||
Signature sig = DSAEngine.getInstance().sign(payload, spk);
|
||||
return sig.getData();
|
||||
}
|
||||
|
||||
public static boolean isValidName(String name) {
|
||||
if (name.length() > Constants.MAX_NICKNAME_LENGTH)
|
||||
return false;
|
||||
for (int i = 0; i < Constants.INVALID_NICKNAME_CHARS.length(); i++)
|
||||
if (name.indexOf(Constants.INVALID_NICKNAME_CHARS.charAt(i)) >= 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -39,13 +39,13 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testHash1Result() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih, 0)
|
||||
byte [] root = new byte[32]
|
||||
SharedFile sf = new SharedFile(f,root, 0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
|
||||
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
|
||||
|
||||
manager.onSearchEvent(se)
|
||||
Thread.sleep(20)
|
||||
@@ -58,14 +58,14 @@ class FileManagerTest {
|
||||
|
||||
@Test
|
||||
void testHash2Results() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
byte [] root = new byte[32]
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), root, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), root, 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
SearchEvent se = new SearchEvent(searchHash: ih.getRoot(), uuid: uuid)
|
||||
SearchEvent se = new SearchEvent(searchHash: root, uuid: uuid)
|
||||
|
||||
manager.onSearchEvent(se)
|
||||
Thread.sleep(20)
|
||||
@@ -81,7 +81,7 @@ class FileManagerTest {
|
||||
void testHash0Results() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih, 0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(), 0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -95,7 +95,7 @@ class FileManagerTest {
|
||||
void testKeyword1Result() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih,0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -113,12 +113,12 @@ class FileManagerTest {
|
||||
void testKeyword2Results() {
|
||||
File f1 = new File("a b.c")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
UUID uuid = UUID.randomUUID()
|
||||
@@ -136,7 +136,7 @@ class FileManagerTest {
|
||||
void testKeyword0Results() {
|
||||
File f = new File("a b.c")
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf = new SharedFile(f,ih,0)
|
||||
SharedFile sf = new SharedFile(f,ih.getRoot(),0)
|
||||
FileHashedEvent fhe = new FileHashedEvent(sharedFile: sf)
|
||||
manager.onFileHashedEvent(fhe)
|
||||
|
||||
@@ -149,8 +149,8 @@ class FileManagerTest {
|
||||
@Test
|
||||
void testRemoveFileExistingHash() {
|
||||
InfoHash ih = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih, 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih, 0)
|
||||
SharedFile sf1 = new SharedFile(new File("a b.c"), ih.getRoot(), 0)
|
||||
SharedFile sf2 = new SharedFile(new File("d e.f"), ih.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf1)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile : sf2)
|
||||
|
||||
@@ -167,12 +167,12 @@ class FileManagerTest {
|
||||
void testRemoveFile() {
|
||||
File f1 = new File("a b.c")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf1)
|
||||
|
||||
File f2 = new File("c d.e")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
manager.onFileLoadedEvent new FileLoadedEvent(loadedFile: sf2)
|
||||
|
||||
manager.onFileUnsharedEvent new FileUnsharedEvent(deleted : true, unsharedFile: sf2)
|
||||
@@ -198,7 +198,7 @@ class FileManagerTest {
|
||||
comment = Base64.encode(DataUtil.encodei18nString(comment))
|
||||
File f1 = new File("MuWire-0.5.10.AppImage")
|
||||
InfoHash ih1 = InfoHash.fromHashList(new byte[32])
|
||||
SharedFile sf1 = new SharedFile(f1, ih1, 0)
|
||||
SharedFile sf1 = new SharedFile(f1, ih1.getRoot(), 0)
|
||||
sf1.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf1))
|
||||
@@ -206,7 +206,7 @@ class FileManagerTest {
|
||||
|
||||
File f2 = new File("MuWire-0.6.0.AppImage")
|
||||
InfoHash ih2 = InfoHash.fromHashList(new byte[64])
|
||||
SharedFile sf2 = new SharedFile(f2, ih2, 0)
|
||||
SharedFile sf2 = new SharedFile(f2, ih2.getRoot(), 0)
|
||||
sf2.setComment(comment)
|
||||
|
||||
manager.onFileLoadedEvent(new FileLoadedEvent(loadedFile : sf2))
|
||||
|
@@ -45,7 +45,7 @@ class HasherServiceTest {
|
||||
def hashed = listener.poll()
|
||||
assert hashed instanceof FileHashedEvent
|
||||
assert hashed.sharedFile.file == f.getCanonicalFile()
|
||||
assert hashed.sharedFile.infoHash != null
|
||||
assert hashed.sharedFile.root != null
|
||||
assert listener.isEmpty()
|
||||
}
|
||||
|
||||
|
@@ -85,7 +85,7 @@ class PersisterServiceLoadingTest {
|
||||
def loadedFile = listener.publishedFiles[0]
|
||||
assert loadedFile != null
|
||||
assert loadedFile.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile.infoHash == ih1
|
||||
assert loadedFile.root == ih1.getRoot()
|
||||
}
|
||||
|
||||
private static String getSharedFileJsonName(File sharedFile) {
|
||||
@@ -128,7 +128,7 @@ class PersisterServiceLoadingTest {
|
||||
def loadedFile = listener.publishedFiles[0]
|
||||
assert loadedFile != null
|
||||
assert loadedFile.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile.infoHash == ih1
|
||||
assert loadedFile.root == ih1.getRoot()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -169,10 +169,10 @@ class PersisterServiceLoadingTest {
|
||||
assert listener.publishedFiles.size() == 2
|
||||
def loadedFile1 = listener.publishedFiles[0]
|
||||
assert loadedFile1.file == sharedFile1.getCanonicalFile()
|
||||
assert loadedFile1.infoHash == ih1
|
||||
assert loadedFile1.root == ih1.getRoot()
|
||||
def loadedFile2 = listener.publishedFiles[1]
|
||||
assert loadedFile2.file == sharedFile2.getCanonicalFile()
|
||||
assert loadedFile2.infoHash == ih2
|
||||
assert loadedFile2.root == ih2.getRoot()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -2,6 +2,7 @@ package com.muwire.core.files
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
import com.muwire.core.Destinations
|
||||
@@ -16,6 +17,7 @@ import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base32
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Ignore
|
||||
class PersisterServiceSavingTest {
|
||||
|
||||
File f
|
||||
|
49
doc/collections.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# MuWire Collections
|
||||
Status: Draft, Proposal, Unimplemented
|
||||
|
||||
MuWire collections are files containing meta-information about a grouping of files. They serve a similar purpose like .torrent files, but the internal format is rather different to account for the MuWire identity management.
|
||||
|
||||
A user wishing to create a collection of files needs to have shared all the files that are going to be part of the collection. Their full MuWire ID will be stored in the collection, so anyone wishing to download any of the files in the collection will try to download from them first.
|
||||
|
||||
The collection will be signed, so anyone can verify that the embedded full MuWire ID authored the collection.
|
||||
|
||||
### File Format
|
||||
|
||||
Header:
|
||||
|
||||
```
|
||||
byte 0: Collection version, currently fixed at "1".
|
||||
bytes 1,2 : unsigned 16-bit value of the number of files in the collection. Empty files or directories are not allowed.
|
||||
bytes 3-N: Full MuWire ID of the publisher of the collection, in Persona format.
|
||||
bytes N+1 to N+9: Timestamp of the collection, in milliseconds since epoch UTC
|
||||
bytes N+9 to M: Free-form description of the collection (comment). Format is UTF-8, maximum size is 32kb.
|
||||
```
|
||||
|
||||
The header is followed by a file entry for each file in the collection. The format is the follows:
|
||||
```
|
||||
byte 0: File entry version, currently fixed at "1".
|
||||
byte 1-33: hash of the file
|
||||
byte 34: Unsigned 8-bit number of path elements from root to where the file will ultimately be placed upon download.
|
||||
bytes 35-N : UTF-8 encoded length-prefixed path elements. Each element can be at most 32kb long. The last element is the name of the file.
|
||||
bytes N-M: free from description of the file (comment). Format is UTF-8, maximum size is 32kb.
|
||||
```
|
||||
|
||||
After the file entries follows a footer, which is simply a signature of the byte payload of the header and the file entries.
|
||||
|
||||
### Downloading
|
||||
|
||||
Since the collection is created from individual shared files, every file within the collection is searchable. It is possible to extend the shared file data structure to contain refererences to any collections the file belongs to - TBD.
|
||||
|
||||
When a user searches for a keyword or hash, they can find either the collection metafile itself or a file which is a member of one or more collections. In the latter case, the user is given the option to download the collection metafile.
|
||||
|
||||
If the user chooses to download the collection metafile, they will be presented with a dialog containing the metainformation contained in the collection descriptor. They will be able to see directory structure contained in the collection and will be able to choose individual files to download.
|
||||
|
||||
TBD - what happens when some of the files are already downloaded but are not in the final directory location?
|
||||
|
||||
Finally, when starting the download, the downloader always queries the persona in the collection first, regardless of who returned the search result.
|
||||
|
||||
### Sharing
|
||||
|
||||
When downloading the collection descriptor, the user makes the descriptor available for indexing. This way collection descriptors can propagate on the network.
|
||||
TBD - do they also index the comments and file names in the descriptor, even if they haven't downloaded the files?
|
||||
|
@@ -1,15 +1,17 @@
|
||||
group = com.muwire
|
||||
version = 0.6.11
|
||||
i2pVersion = 0.9.44
|
||||
groovyVersion = 2.4.15
|
||||
version = 0.7.1
|
||||
i2pVersion = 0.9.46
|
||||
groovyVersion = 3.0.4
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
grailsVersion=4.0.0
|
||||
gorm.version=7.0.2.RELEASE
|
||||
griffonEnv=prod
|
||||
|
||||
# javac properties
|
||||
sourceCompatibility=1.8
|
||||
targetCompatibility=1.8
|
||||
compilerArgs=-Xlint:unchecked,cast,path,divzero,empty,path,finally,overrides
|
||||
|
||||
# plugin properties
|
||||
author = zab@mail.i2p
|
||||
@@ -18,4 +20,4 @@ keystorePassword=changeit
|
||||
websiteURL=http://muwire.i2p
|
||||
updateURLsu3=http://muwire.i2p/MuWire-update.su3
|
||||
|
||||
pack200=true
|
||||
pack200=false
|
||||
|
@@ -42,10 +42,25 @@ griffon {
|
||||
|
||||
application {
|
||||
mainClassName = 'com.muwire.gui.Launcher'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties','-Xmx256M']
|
||||
applicationName = 'MuWire'
|
||||
}
|
||||
|
||||
run {
|
||||
applicationDefaultJvmArgs=[]
|
||||
}
|
||||
|
||||
startScripts.doFirst {
|
||||
application.applicationDefaultJvmArgs = ["-Djava.util.logging.config.file=logging.properties",
|
||||
"-Xmx256M",
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/sun.nio.fs=ALL-UNNAMED",
|
||||
"--add-opens", "java.base/java.nio=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/java.awt=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/javax.swing=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/javax.swing.plaf.basic=ALL-UNNAMED"]
|
||||
|
||||
}
|
||||
|
||||
apply from: 'gradle/publishing.gradle'
|
||||
// apply from: 'gradle/code-coverage.gradle'
|
||||
// apply from: 'gradle/code-quality.gradle'
|
||||
@@ -57,9 +72,27 @@ apply plugin: 'org.kordamp.gradle.stats'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'com.github.kt3k.coveralls'
|
||||
|
||||
|
||||
configurations.all {
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-test'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-testng'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-test-junit5'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-ant'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-sql'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-nio'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-servlet'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-jmx'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-groovydoc'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-groovysh'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-xml'
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-docgenerator'
|
||||
// TODO: add more as discovered
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
|
||||
compile "org.codehaus.groovy:groovy-all:${groovyVersion}"
|
||||
|
||||
// runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
|
||||
|
||||
|
@@ -131,4 +131,19 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.FeedConfigurationView'
|
||||
controller = 'com.muwire.gui.FeedConfigurationController'
|
||||
}
|
||||
'watched-directory' {
|
||||
model = 'com.muwire.gui.WatchedDirectoryModel'
|
||||
view = 'com.muwire.gui.WatchedDirectoryView'
|
||||
controller = 'com.muwire.gui.WatchedDirectoryController'
|
||||
}
|
||||
'sign' {
|
||||
model = 'com.muwire.gui.SignModel'
|
||||
view = 'com.muwire.gui.SignView'
|
||||
controller = 'com.muwire.gui.SignController'
|
||||
}
|
||||
'wizard' {
|
||||
model = 'com.muwire.gui.wizard.WizardModel'
|
||||
view = 'com.muwire.gui.wizard.WizardView'
|
||||
controller = 'com.muwire.gui.wizard.WizardController'
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.directories.UISyncDirectoryEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class AdvancedSharingController {
|
||||
@@ -14,4 +15,25 @@ class AdvancedSharingController {
|
||||
AdvancedSharingModel model
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingView view
|
||||
|
||||
@ControllerAction
|
||||
void configure() {
|
||||
def wd = view.selectedWatchedDirectory()
|
||||
if (wd == null)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['core'] = model.core
|
||||
params['directory'] = wd
|
||||
mvcGroup.createMVCGroup("watched-directory",params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void sync() {
|
||||
def wd = view.selectedWatchedDirectory()
|
||||
if (wd == null)
|
||||
return
|
||||
def event = new UISyncDirectoryEvent(directory : wd.directory)
|
||||
model.core.eventBus.publish(event)
|
||||
}
|
||||
}
|
@@ -151,6 +151,7 @@ class MainFrameController {
|
||||
params["search-terms"] = tabTitle
|
||||
params["uuid"] = uuid.toString()
|
||||
params["core"] = core
|
||||
params["settings"] = view.settings
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import java.util.logging.Level
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.JOptionPane
|
||||
import java.awt.Font
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
@@ -32,22 +33,14 @@ class OptionsController {
|
||||
|
||||
def i2pProps = core.i2pOptions
|
||||
|
||||
text = view.inboundLengthField.text
|
||||
model.inboundLength = text
|
||||
i2pProps["inbound.length"] = text
|
||||
|
||||
text = view.inboundQuantityField.text
|
||||
model.inboundQuantity = text
|
||||
i2pProps["inbound.quantity"] = text
|
||||
|
||||
text = view.outboundQuantityField.text
|
||||
model.outboundQuantity = text
|
||||
i2pProps["outbound.quantity"] = text
|
||||
|
||||
text = view.outboundLengthField.text
|
||||
model.outboundLength = text
|
||||
i2pProps["outbound.length"] = text
|
||||
|
||||
int tunnelLength = view.tunnelLengthSlider.value
|
||||
i2pProps["inbound.length"] = String.valueOf(tunnelLength)
|
||||
i2pProps["outbound.length"] = String.valueOf(tunnelLength)
|
||||
|
||||
int tunnelQuantity = view.tunnelQuantitySlider.value
|
||||
i2pProps["inbound.quantity"] = String.valueOf(tunnelQuantity)
|
||||
i2pProps["outbound.quantity"] = String.valueOf(tunnelQuantity)
|
||||
|
||||
if (settings.embeddedRouter) {
|
||||
text = view.i2pNTCPPortField.text
|
||||
model.i2pNTCPPort = text
|
||||
@@ -104,6 +97,10 @@ class OptionsController {
|
||||
model.browseFiles = browseFiles
|
||||
settings.browseFiles = browseFiles
|
||||
|
||||
boolean allowTracking = view.allowTrackingCheckbox.model.isSelected()
|
||||
model.allowTracking = allowTracking
|
||||
settings.allowTracking = allowTracking
|
||||
|
||||
text = view.speedSmoothSecondsField.text
|
||||
model.speedSmoothSeconds = Integer.valueOf(text)
|
||||
settings.speedSmoothSeconds = Integer.valueOf(text)
|
||||
@@ -204,6 +201,12 @@ class OptionsController {
|
||||
|
||||
uiSettings.autoFontSize = model.automaticFontSize
|
||||
uiSettings.fontSize = Integer.parseInt(view.fontSizeField.text)
|
||||
|
||||
uiSettings.fontStyle = Font.PLAIN
|
||||
if (view.fontStyleBoldCheckbox.model.isSelected())
|
||||
uiSettings.fontStyle |= Font.BOLD
|
||||
if (view.fontStyleItalicCheckbox.model.isSelected())
|
||||
uiSettings.fontStyle |= Font.ITALIC
|
||||
|
||||
uiSettings.groupByFile = model.groupByFile
|
||||
|
||||
|
@@ -0,0 +1,50 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class SignController {
|
||||
|
||||
Core core
|
||||
|
||||
@MVCMember @Nonnull
|
||||
SignView view
|
||||
|
||||
@ControllerAction
|
||||
void sign() {
|
||||
String plain = view.plainTextArea.getText()
|
||||
byte[] payload = plain.trim().getBytes(StandardCharsets.UTF_8)
|
||||
def sig = DSAEngine.getInstance().sign(payload, core.spk)
|
||||
view.signedTextArea.setText(Base64.encode(sig.data))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void copy() {
|
||||
String signed = view.signedTextArea.getText()
|
||||
StringSelection selection = new StringSelection(signed)
|
||||
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
clipboard.setContents(selection, null)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void close() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class WatchedDirectoryController {
|
||||
@MVCMember @Nonnull
|
||||
WatchedDirectoryModel model
|
||||
@MVCMember @Nonnull
|
||||
WatchedDirectoryView view
|
||||
|
||||
@ControllerAction
|
||||
void save() {
|
||||
def event = new WatchedDirectoryConfigurationEvent(
|
||||
directory : model.directory.directory,
|
||||
autoWatch : view.autoWatchCheckbox.model.isSelected(),
|
||||
syncInterval : Integer.parseInt(view.syncIntervalField.text))
|
||||
model.core.eventBus.publish(event)
|
||||
cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class WizardController {
|
||||
@MVCMember @Nonnull
|
||||
WizardModel model
|
||||
@MVCMember @Nonnull
|
||||
WizardView view
|
||||
|
||||
@ControllerAction
|
||||
void previous() {
|
||||
model.currentStep--
|
||||
view.updateLayout()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void next() {
|
||||
def errors = model.steps[model.currentStep].validate()
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
String errorMessage = String.join("\n", errors)
|
||||
JOptionPane.showMessageDialog(model.parent, errorMessage, "Invalid Input", JOptionPane.ERROR_MESSAGE)
|
||||
} else {
|
||||
model.currentStep++
|
||||
view.updateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void finish() {
|
||||
model.steps.each {
|
||||
it.apply(model.muSettings, model.i2pProps)
|
||||
}
|
||||
model.finished['applied'] = true
|
||||
view.hide()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
model.finished['applied'] = false
|
||||
view.hide()
|
||||
}
|
||||
}
|
@@ -40,15 +40,6 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
@Override
|
||||
void execute() {
|
||||
|
||||
if (System.getProperty("java.util.logging.config.file") == null) {
|
||||
log.info("No config file specified, so turning off most logging")
|
||||
def names = LogManager.getLogManager().getLoggerNames()
|
||||
while(names.hasMoreElements()) {
|
||||
def name = names.nextElement()
|
||||
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
|
||||
}
|
||||
}
|
||||
|
||||
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
|
||||
|
||||
if (SystemTray.isSupported() && (SystemVersion.isMac() || SystemVersion.isWindows())) {
|
||||
@@ -133,7 +124,7 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
uiSettings.lnf = lnf.getID()
|
||||
}
|
||||
|
||||
if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0) {
|
||||
if (uiSettings.font != null || uiSettings.autoFontSize || uiSettings.fontSize > 0 ) {
|
||||
|
||||
FontUIResource defaultFont = lnf.getDefaults().getFont("Label.font")
|
||||
|
||||
@@ -151,7 +142,7 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
fontSize = uiSettings.fontSize
|
||||
}
|
||||
rowHeight = fontSize + 3
|
||||
FontUIResource font = new FontUIResource(fontName, Font.PLAIN, fontSize)
|
||||
FontUIResource font = new FontUIResource(fontName, uiSettings.fontStyle, fontSize)
|
||||
|
||||
def keys = lnf.getDefaults().keys()
|
||||
while(keys.hasMoreElements()) {
|
||||
@@ -167,21 +158,23 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
Properties props = new Properties()
|
||||
uiSettings = new UISettings(props)
|
||||
log.info "will try default lnfs"
|
||||
LookAndFeel chosen
|
||||
if (isMacOSX()) {
|
||||
if (SystemVersion.isJava9()) {
|
||||
uiSettings.lnf = "metal"
|
||||
lookAndFeel("metal")
|
||||
} else {
|
||||
uiSettings.lnf = "nimbus"
|
||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||
}
|
||||
chosen = lookAndFeel("metal")
|
||||
} else {
|
||||
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||
chosen = lookAndFeel('system', 'gtk')
|
||||
if (chosen == null)
|
||||
chosen = lookAndFeel('metal')
|
||||
uiSettings.lnf = chosen.getID()
|
||||
log.info("ended up applying $chosen.name")
|
||||
}
|
||||
|
||||
FontUIResource defaultFont = chosen.getDefaults().getFont("Label.font")
|
||||
uiSettings.font = defaultFont.getName()
|
||||
uiSettings.fontSize = defaultFont.getSize()
|
||||
uiSettings.fontStyle = defaultFont.getStyle()
|
||||
rowHeight = uiSettings.fontSize + 3
|
||||
}
|
||||
|
||||
application.context.put("row-height", rowHeight)
|
||||
|
@@ -6,10 +6,13 @@ import net.i2p.util.SystemVersion
|
||||
|
||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
import com.muwire.gui.wizard.WizardDefaults
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
@@ -44,36 +47,49 @@ class Ready extends AbstractLifecycleHandler {
|
||||
propsFile.withReader("UTF-8", {
|
||||
props.load(it)
|
||||
})
|
||||
if (!props.containsKey("nickname"))
|
||||
props.setProperty("nickname", selectNickname())
|
||||
props = new MuWireSettings(props)
|
||||
if (props.incompleteLocation == null)
|
||||
props.incompleteLocation = new File(home, "incompletes")
|
||||
} else {
|
||||
log.info("creating new properties")
|
||||
props = new MuWireSettings()
|
||||
props.incompleteLocation = new File(home, "incompletes")
|
||||
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||
props.updateType = System.getProperty("updateType","jar")
|
||||
props.setNickname(selectNickname())
|
||||
|
||||
|
||||
def portableDownloads = System.getProperty("portable.downloads")
|
||||
if (portableDownloads != null) {
|
||||
props.downloadLocation = new File(portableDownloads)
|
||||
} else {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(false)
|
||||
chooser.setDialogTitle("Select a directory where downloads will be saved")
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv != JFileChooser.APPROVE_OPTION) {
|
||||
JOptionPane.showMessageDialog(null, "MuWire will now exit")
|
||||
System.exit(0)
|
||||
}
|
||||
props.downloadLocation = chooser.getSelectedFile()
|
||||
boolean embeddedRouterAvailable = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
|
||||
|
||||
def defaults
|
||||
if (System.getProperties().containsKey("wizard.defaults")) {
|
||||
File defaultsFile = new File(System.getProperty("wizard.defaults"))
|
||||
Properties defaultsProps = new Properties()
|
||||
defaultsFile.withInputStream { defaultsProps.load(it) }
|
||||
defaults = new WizardDefaults(defaultsProps)
|
||||
} else
|
||||
defaults = new WizardDefaults()
|
||||
|
||||
def parent = application.windowManager.findWindow("event-list")
|
||||
Properties i2pProps = new Properties()
|
||||
|
||||
def params = [:]
|
||||
params['parent'] = parent
|
||||
params['defaults'] = defaults
|
||||
params['embeddedRouterAvailable'] = embeddedRouterAvailable
|
||||
params['muSettings'] = props
|
||||
params['i2pProps'] = i2pProps
|
||||
def finished = [:]
|
||||
params['finished'] = finished
|
||||
|
||||
application.mvcGroupManager.createMVCGroup("wizard", params)
|
||||
|
||||
if (!finished['applied']) {
|
||||
JOptionPane.showMessageDialog(parent, "MuWire will now exit")
|
||||
System.exit(0)
|
||||
}
|
||||
|
||||
|
||||
File i2pPropsFile = new File(home, "i2p.properties")
|
||||
i2pPropsFile.withPrintWriter { i2pProps.store(it, "") }
|
||||
|
||||
props.embeddedRouter = embeddedRouterAvailable
|
||||
props.updateType = System.getProperty("updateType","jar")
|
||||
|
||||
|
||||
propsFile.withPrintWriter("UTF-8", {
|
||||
props.write(it)
|
||||
})
|
||||
@@ -100,31 +116,5 @@ class Ready extends AbstractLifecycleHandler {
|
||||
System.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
private String selectNickname() {
|
||||
String nickname
|
||||
while (true) {
|
||||
nickname = JOptionPane.showInputDialog(null,
|
||||
"Your nickname is displayed when you send search results so other MuWire users can choose to trust you",
|
||||
"Please choose a nickname", JOptionPane.PLAIN_MESSAGE)
|
||||
if (nickname == null) {
|
||||
JOptionPane.showMessageDialog(null, "MuWire cannot start without a nickname and will now exit", JOptionPane.PLAIN_MESSAGE)
|
||||
System.exit(0)
|
||||
}
|
||||
if (nickname.trim().length() == 0) {
|
||||
JOptionPane.showMessageDialog(null, "Nickname cannot be empty", "Select another nickname",
|
||||
JOptionPane.WARNING_MESSAGE)
|
||||
continue
|
||||
}
|
||||
if (nickname.contains("@")) {
|
||||
JOptionPane.showMessageDialog(null, "Nickname cannot contain @, choose another",
|
||||
"Select another nickname", JOptionPane.WARNING_MESSAGE)
|
||||
continue
|
||||
}
|
||||
nickname = nickname.trim()
|
||||
break
|
||||
}
|
||||
nickname
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,32 +1,49 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.tree.DefaultMutableTreeNode
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
import javax.swing.tree.MutableTreeNode
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.FileTree
|
||||
import com.muwire.core.files.directories.WatchedDirectoryConfigurationEvent
|
||||
import com.muwire.core.files.directories.WatchedDirectorySyncEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class AdvancedSharingModel {
|
||||
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingView view
|
||||
|
||||
def watchedDirectories = []
|
||||
def treeRoot
|
||||
def negativeTree
|
||||
|
||||
Core core
|
||||
|
||||
@Observable boolean syncActionEnabled
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
watchedDirectories.addAll(core.muOptions.watchedDirectories)
|
||||
watchedDirectories.addAll(core.watchedDirectoryManager.watchedDirs.values())
|
||||
core.eventBus.register(WatchedDirectorySyncEvent.class, this)
|
||||
core.eventBus.register(WatchedDirectoryConfigurationEvent.class, this)
|
||||
|
||||
treeRoot = new DefaultMutableTreeNode()
|
||||
negativeTree = new DefaultTreeModel(treeRoot)
|
||||
copyTree(treeRoot, core.fileManager.negativeTree.root)
|
||||
}
|
||||
|
||||
void mvcGroupDestroy() {
|
||||
core.eventBus.unregister(WatchedDirectorySyncEvent.class, this)
|
||||
core.eventBus.unregister(WatchedDirectoryConfigurationEvent.class, this)
|
||||
}
|
||||
|
||||
private void copyTree(DefaultMutableTreeNode jtreeNode, FileTree.TreeNode fileTreeNode) {
|
||||
jtreeNode.setUserObject(fileTreeNode.file?.getName())
|
||||
fileTreeNode.children.each {
|
||||
@@ -36,4 +53,16 @@ class AdvancedSharingModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectorySyncEvent(WatchedDirectorySyncEvent e) {
|
||||
runInsideUIAsync {
|
||||
view.watchedDirsTable.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
void onWatchedDirectoryConfigurationEvent(WatchedDirectoryConfigurationEvent e) {
|
||||
runInsideUIAsync {
|
||||
view.watchedDirsTable.model.fireTableDataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -294,8 +294,6 @@ class MainFrameModel {
|
||||
|
||||
void onAllFilesLoadedEvent(AllFilesLoadedEvent e) {
|
||||
runInsideUIAsync {
|
||||
core.muOptions.watchedDirectories.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
|
||||
|
||||
core.muOptions.trustSubscriptions.each {
|
||||
core.eventBus.publish(new TrustSubscriptionEvent(persona : it, subscribe : true))
|
||||
}
|
||||
@@ -415,7 +413,7 @@ class MainFrameModel {
|
||||
break
|
||||
if (parent.getChildCount() == 0) {
|
||||
File file = parent.getUserObject().file
|
||||
if (core.muOptions.watchedDirectories.contains(file.toString()))
|
||||
if (core.watchedDirectoryManager.isWatched(file))
|
||||
unshared.add(file)
|
||||
dmtn = parent
|
||||
continue
|
||||
|
@@ -7,6 +7,8 @@ import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import java.awt.Font
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class OptionsModel {
|
||||
@Observable String downloadRetryInterval
|
||||
@@ -18,16 +20,15 @@ class OptionsModel {
|
||||
@Observable String incompleteLocation
|
||||
@Observable boolean searchComments
|
||||
@Observable boolean browseFiles
|
||||
@Observable boolean allowTracking
|
||||
@Observable int speedSmoothSeconds
|
||||
@Observable int totalUploadSlots
|
||||
@Observable int uploadSlotsPerUser
|
||||
@Observable boolean storeSearchHistory
|
||||
|
||||
// i2p options
|
||||
@Observable String inboundLength
|
||||
@Observable String inboundQuantity
|
||||
@Observable String outboundLength
|
||||
@Observable String outboundQuantity
|
||||
@Observable int tunnelLength
|
||||
@Observable int tunnelQuantity
|
||||
@Observable String i2pUDPPort
|
||||
@Observable String i2pNTCPPort
|
||||
|
||||
@@ -37,6 +38,8 @@ class OptionsModel {
|
||||
@Observable String font
|
||||
@Observable boolean automaticFontSize
|
||||
@Observable int customFontSize
|
||||
@Observable boolean fontStyleBold
|
||||
@Observable boolean fontStyleItalic
|
||||
@Observable boolean clearCancelledDownloads
|
||||
@Observable boolean clearFinishedDownloads
|
||||
@Observable boolean excludeLocalResult
|
||||
@@ -83,15 +86,14 @@ class OptionsModel {
|
||||
incompleteLocation = settings.incompleteLocation.getAbsolutePath()
|
||||
searchComments = settings.searchComments
|
||||
browseFiles = settings.browseFiles
|
||||
allowTracking = settings.allowTracking
|
||||
speedSmoothSeconds = settings.speedSmoothSeconds
|
||||
totalUploadSlots = settings.totalUploadSlots
|
||||
uploadSlotsPerUser = settings.uploadSlotsPerUser
|
||||
|
||||
Core core = application.context.get("core")
|
||||
inboundLength = core.i2pOptions["inbound.length"]
|
||||
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
||||
outboundLength = core.i2pOptions["outbound.length"]
|
||||
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
||||
tunnelLength = Math.max(Integer.parseInt(core.i2pOptions["inbound.length"]), Integer.parseInt(core.i2pOptions['outbound.length']))
|
||||
tunnelQuantity = Math.max(Integer.parseInt(core.i2pOptions["inbound.quantity"]), Integer.parseInt(core.i2pOptions['outbound.quantity']))
|
||||
i2pUDPPort = core.i2pOptions["i2np.udp.port"]
|
||||
i2pNTCPPort = core.i2pOptions["i2np.ntcp.port"]
|
||||
|
||||
@@ -101,6 +103,8 @@ class OptionsModel {
|
||||
font = uiSettings.font
|
||||
automaticFontSize = uiSettings.autoFontSize
|
||||
customFontSize = uiSettings.fontSize
|
||||
fontStyleBold = (uiSettings.fontStyle & Font.BOLD) == Font.BOLD
|
||||
fontStyleItalic = (uiSettings.fontStyle & Font.ITALIC) == Font.ITALIC
|
||||
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||
excludeLocalResult = uiSettings.excludeLocalResult
|
||||
|
9
gui/griffon-app/models/com/muwire/gui/SignModel.groovy
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class SignModel {
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.files.directories.WatchedDirectory
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class WatchedDirectoryModel {
|
||||
Core core
|
||||
WatchedDirectory directory
|
||||
|
||||
@Observable boolean autoWatch
|
||||
@Observable int syncInterval
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
autoWatch = directory.autoWatch
|
||||
syncInterval = directory.syncInterval
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import java.awt.Component
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class WizardModel {
|
||||
Component parent
|
||||
WizardDefaults defaults
|
||||
boolean embeddedRouterAvailable
|
||||
MuWireSettings muSettings
|
||||
Properties i2pProps
|
||||
def finished
|
||||
|
||||
final List<WizardStep> steps = []
|
||||
int currentStep
|
||||
|
||||
@Observable boolean finishButtonEnabled
|
||||
@Observable boolean previousButtonEnabled
|
||||
@Observable boolean nextButtonEnabled
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
||||
steps << new NicknameStep()
|
||||
steps << new DirectoriesStep(defaults)
|
||||
if (embeddedRouterAvailable)
|
||||
steps << new EmbeddedRouterStep(defaults)
|
||||
else
|
||||
steps << new ExternalRouterStep(defaults)
|
||||
steps << new TunnelStep(defaults)
|
||||
steps << new LastStep(embeddedRouterAvailable)
|
||||
|
||||
currentStep = 0
|
||||
previousButtonEnabled = false
|
||||
nextButtonEnabled = steps.size() > (currentStep + 1)
|
||||
finishButtonEnabled = steps.size() == currentStep + 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -3,13 +3,23 @@ package com.muwire.gui
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.JTree
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.files.directories.WatchedDirectory
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
@@ -21,6 +31,8 @@ class AdvancedSharingView {
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingModel model
|
||||
@MVCMember @Nonnull
|
||||
AdvancedSharingController controller
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
@@ -28,6 +40,7 @@ class AdvancedSharingView {
|
||||
def negativeTreePanel
|
||||
|
||||
def watchedDirsTable
|
||||
def watchedDirsTableSortEvent
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
@@ -43,10 +56,17 @@ class AdvancedSharingView {
|
||||
scrollPane( constraints : BorderLayout.CENTER ) {
|
||||
watchedDirsTable = table(autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.watchedDirectories) {
|
||||
closureColumn(header : "Directory", type : String, read : {it})
|
||||
closureColumn(header : "Directory", preferredWidth: 350, type : String, read : {it.directory.toString()})
|
||||
closureColumn(header : "Auto", preferredWidth: 100, type : Boolean, read : {it.autoWatch})
|
||||
closureColumn(header : "Interval", preferredWidth : 100, type : Integer, read : {it.syncInterval})
|
||||
closureColumn(header : "Last Sync", preferredWidth: 250, type : Long, read : {it.lastSync})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Configure", configureAction)
|
||||
button(text : "Sync", enabled : bind{model.syncActionEnabled}, syncAction)
|
||||
}
|
||||
}
|
||||
|
||||
negativeTreePanel = builder.panel {
|
||||
@@ -59,6 +79,54 @@ class AdvancedSharingView {
|
||||
tree(rootVisible : false, rowHeight : rowHeight,jtree)
|
||||
}
|
||||
}
|
||||
|
||||
def centerRenderer = new DefaultTableCellRenderer()
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
|
||||
watchedDirsTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
watchedDirsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
|
||||
watchedDirsTable.rowSorter.addRowSorterListener({evt -> watchedDirsTableSortEvent = evt})
|
||||
def selectionModel = watchedDirsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
def directory = selectedWatchedDirectory()
|
||||
model.syncActionEnabled = !(directory == null || directory.autoWatch)
|
||||
})
|
||||
|
||||
watchedDirsTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showMenu(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private void showMenu(MouseEvent e) {
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
JMenuItem configure = new JMenuItem("Configure")
|
||||
configure.addActionListener({controller.configure()})
|
||||
menu.add(configure)
|
||||
|
||||
if (model.syncActionEnabled) {
|
||||
JMenuItem sync = new JMenuItem("Sync")
|
||||
sync.addActionListener({controller.sync()})
|
||||
menu.add(sync)
|
||||
}
|
||||
|
||||
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||
}
|
||||
|
||||
WatchedDirectory selectedWatchedDirectory() {
|
||||
int row = watchedDirsTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return null
|
||||
if (watchedDirsTableSortEvent != null)
|
||||
row = watchedDirsTable.rowSorter.convertRowIndexToModel(row)
|
||||
model.watchedDirectories[row]
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
@@ -91,7 +91,7 @@ class MainFrameView {
|
||||
settings = application.context.get("ui-settings")
|
||||
int rowHeight = application.context.get("row-height")
|
||||
builder.with {
|
||||
application(size : [1024,768], id: 'main-frame',
|
||||
application(size : [settings.mainFrameX,settings.mainFrameY], id: 'main-frame',
|
||||
locationRelativeTo : null,
|
||||
defaultCloseOperation : JFrame.DO_NOTHING_ON_CLOSE,
|
||||
title: application.configuration['application.title'] + " " +
|
||||
@@ -144,6 +144,11 @@ class MainFrameView {
|
||||
mvcGroup.createMVCGroup("chat-monitor","chat-monitor",env)
|
||||
}
|
||||
})
|
||||
menuItem("Sign Tool", actionPerformed : {
|
||||
def env = [:]
|
||||
env['core'] = model.core
|
||||
mvcGroup.createMVCGroup("sign",env)
|
||||
})
|
||||
}
|
||||
}
|
||||
borderLayout()
|
||||
@@ -163,7 +168,7 @@ class MainFrameView {
|
||||
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
label(constraints : "top-connect-panel",
|
||||
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
|
||||
text : " MuWire is connecting, please wait.") // TODO: real padding
|
||||
panel(constraints : "top-search-panel") {
|
||||
borderLayout()
|
||||
panel(constraints: BorderLayout.CENTER) {
|
||||
@@ -324,16 +329,16 @@ class MainFrameView {
|
||||
}
|
||||
panel {
|
||||
gridBagLayout()
|
||||
button(text : "Share", constraints : gbc(gridx: 0), actionPerformed : shareFiles)
|
||||
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 1), addCommentAction)
|
||||
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 2), issueCertificateAction)
|
||||
button(text : bind {model.publishButtonText}, enabled : bind {model.publishButtonEnabled}, constraints : gbc(gridx:3), publishAction)
|
||||
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 0), addCommentAction)
|
||||
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, constraints : gbc(gridx: 1), issueCertificateAction)
|
||||
button(text : bind {model.publishButtonText}, enabled : bind {model.publishButtonEnabled}, constraints : gbc(gridx:2), publishAction)
|
||||
}
|
||||
panel {
|
||||
panel {
|
||||
label("Shared:")
|
||||
label(text : bind {model.loadedFiles}, id : "shared-files-count")
|
||||
}
|
||||
button(text : "Share", actionPerformed : shareFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,7 +452,7 @@ class MainFrameView {
|
||||
closureColumn(header : "Publisher", preferredWidth: 350, type : String, read : {it.getPublisher().getHumanReadableName()})
|
||||
closureColumn(header : "Files", preferredWidth: 10, type : Integer, read : {model.core.feedManager.getFeedItems(it.getPublisher()).size()})
|
||||
closureColumn(header : "Last Updated", type : Long, read : {it.getLastUpdated()})
|
||||
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus()})
|
||||
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus().toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,9 +565,10 @@ class MainFrameView {
|
||||
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.WEST) {
|
||||
label(text : bind {model.me})
|
||||
button(text : "Copy Short", copyShortAction)
|
||||
button(text : "Copy Full", copyFullAction)
|
||||
gridBagLayout()
|
||||
label(text : bind {model.me}, constraints : gbc(gridx:0, gridy:0))
|
||||
button(text : "Copy Short", constraints : gbc(gridx:1, gridy:0), copyShortAction)
|
||||
button(text : "Copy Full", constraints : gbc(gridx:2, gridy:0), copyFullAction)
|
||||
}
|
||||
panel (constraints : BorderLayout.EAST) {
|
||||
label("Connections:")
|
||||
@@ -1451,10 +1457,10 @@ class MainFrameView {
|
||||
for (int i = 0; i < count; i++)
|
||||
settings.openTabs.add(tabbedPane.getTitleAt(i))
|
||||
|
||||
File uiPropsFile = new File(core.home, "gui.properties")
|
||||
uiPropsFile.withOutputStream { settings.write(it) }
|
||||
|
||||
def mainFrame = builder.getVariable("main-frame")
|
||||
JFrame mainFrame = builder.getVariable("main-frame")
|
||||
settings.mainFrameX = mainFrame.getSize().width
|
||||
settings.mainFrameY = mainFrame.getSize().height
|
||||
mainFrame.setVisible(false)
|
||||
application.getWindowManager().findWindow("shutdown-window").setVisible(true)
|
||||
if (core != null) {
|
||||
@@ -1464,6 +1470,9 @@ class MainFrameView {
|
||||
}as Runnable)
|
||||
t.start()
|
||||
}
|
||||
|
||||
File uiPropsFile = new File(core.home, "gui.properties")
|
||||
uiPropsFile.withOutputStream { settings.write(it) }
|
||||
}
|
||||
|
||||
private static class TreeExpansions implements TreeExpansionListener {
|
||||
|
@@ -6,7 +6,9 @@ import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.swing.factory.TitledBorderFactory
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JSlider
|
||||
import javax.swing.JTabbedPane
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.border.TitledBorder
|
||||
@@ -43,14 +45,13 @@ class OptionsView {
|
||||
def shareHiddenCheckbox
|
||||
def searchCommentsCheckbox
|
||||
def browseFilesCheckbox
|
||||
def allowTrackingCheckbox
|
||||
def speedSmoothSecondsField
|
||||
def totalUploadSlotsField
|
||||
def uploadSlotsPerUserField
|
||||
|
||||
def inboundLengthField
|
||||
def inboundQuantityField
|
||||
def outboundLengthField
|
||||
def outboundQuantityField
|
||||
def tunnelLengthSlider
|
||||
def tunnelQuantitySlider
|
||||
def i2pUDPPortField
|
||||
def i2pNTCPPortField
|
||||
|
||||
@@ -58,6 +59,8 @@ class OptionsView {
|
||||
def monitorCheckbox
|
||||
def fontField
|
||||
def fontSizeField
|
||||
def fontStyleBoldCheckbox
|
||||
def fontStyleItalicCheckbox
|
||||
def clearCancelledDownloadsCheckbox
|
||||
def clearFinishedDownloadsCheckbox
|
||||
def excludeLocalResultCheckbox
|
||||
@@ -107,6 +110,10 @@ class OptionsView {
|
||||
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
|
||||
browseFilesCheckbox = checkBox(selected : bind {model.browseFiles}, constraints : gbc(gridx : 1, gridy : 1,
|
||||
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx: 0))
|
||||
label(text : "Allow tracking", constraints : gbc(gridx: 0, gridy: 2, anchor: GridBagConstraints.LINE_START,
|
||||
fill : GridBagConstraints.HORIZONTAL, weightx: 100))
|
||||
allowTrackingCheckbox = checkBox(selected : bind {model.allowTracking}, constraints : gbc(gridx: 1, gridy : 2,
|
||||
anchor : GridBagConstraints.LINE_END, fill : GridBagConstraints.HORIZONTAL, weightx : 0))
|
||||
}
|
||||
|
||||
panel (border : titledBorder(title : "Download Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
@@ -163,19 +170,24 @@ class OptionsView {
|
||||
label(text : "Changing any I2P settings requires a restart", constraints : gbc(gridx:0, gridy : 0))
|
||||
panel (border : titledBorder(title : "Tunnel Settings", border : etchedBorder(), titlePosition: TitledBorder.TOP,
|
||||
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
|
||||
gridBagLayout()
|
||||
label(text : "Inbound length", constraints : gbc(gridx:0, gridy:0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
inboundLengthField = textField(text : bind {model.inboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:0,
|
||||
anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Inbound quantity", constraints : gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
inboundQuantityField = textField(text : bind {model.inboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:1,
|
||||
anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Outbound length", constraints : gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
outboundLengthField = textField(text : bind {model.outboundLength}, columns : 2, constraints : gbc(gridx:1, gridy:2,
|
||||
anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Outbound quantity", constraints : gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:3,
|
||||
anchor : GridBagConstraints.LINE_END))
|
||||
gridLayout(rows:4, cols:1)
|
||||
|
||||
label(text : "Speed vs Anonymity")
|
||||
def lengthTable = new Hashtable()
|
||||
lengthTable.put(1, new JLabel("Max Speed"))
|
||||
lengthTable.put(3, new JLabel("Max Anonymity"))
|
||||
tunnelLengthSlider = slider(minimum : 1, maximum : 3, value : bind {model.tunnelLength},
|
||||
majorTickSpacing : 1, snapToTicks: true, paintTicks: true, labelTable : lengthTable,
|
||||
paintLabels : true)
|
||||
|
||||
|
||||
label(text: "Reliability vs Resource Usage")
|
||||
def quantityTable = new Hashtable()
|
||||
quantityTable.put(1, new JLabel("Min Resources"))
|
||||
quantityTable.put(6, new JLabel("Max Reliability"))
|
||||
tunnelQuantitySlider = slider(minimum : 1, maximum : 6, value : bind {model.tunnelQuantity},
|
||||
majorTickSpacing : 1, snapToTicks : true, paintTicks: true, labelTable : quantityTable,
|
||||
paintLabels : true)
|
||||
}
|
||||
|
||||
Core core = application.context.get("core")
|
||||
@@ -210,6 +222,14 @@ class OptionsView {
|
||||
constraints : gbc(gridx : 2, gridy: 2, anchor : GridBagConstraints.LINE_START), customFontAction)
|
||||
fontSizeField = textField(text : bind {model.customFontSize}, enabled : bind {!model.automaticFontSize},
|
||||
constraints : gbc(gridx : 3, gridy : 2, anchor : GridBagConstraints.LINE_END))
|
||||
|
||||
label(text : "Font style", constraints: gbc(gridx: 0, gridy: 3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
panel(constraints : gbc(gridx: 2, gridy: 3, gridwidth: 2, anchor:GridBagConstraints.LINE_END)) {
|
||||
fontStyleBoldCheckbox = checkBox(selected : bind {model.fontStyleBold})
|
||||
label(text: "Bold")
|
||||
fontStyleItalicCheckbox = checkBox(selected : bind {model.fontStyleItalic})
|
||||
label(text: "Italic")
|
||||
}
|
||||
|
||||
}
|
||||
panel (border : titledBorder(title : "Search Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
|
66
gui/griffon-app/views/com/muwire/gui/SignView.groovy
Normal file
@@ -0,0 +1,66 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class SignView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
|
||||
def mainFrame
|
||||
def dialog
|
||||
def p
|
||||
def plainTextArea
|
||||
def signedTextArea
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
|
||||
dialog = new JDialog(mainFrame, "Sign Text", true)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label("Enter text to be signed")
|
||||
}
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
gridLayout(rows : 2, cols: 1)
|
||||
scrollPane {
|
||||
plainTextArea = textArea(rows : 10, columns : 50, editable : true, lineWrap: true, wrapStyleWord : true)
|
||||
}
|
||||
scrollPane {
|
||||
signedTextArea = textArea(rows : 10, columns : 50, editable : false, lineWrap : true, wrapStyleWord : true)
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Sign", signAction)
|
||||
button(text : "Copy To Clipboard", copyAction)
|
||||
button(text : "Dismiss", closeAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener( new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.event.ChangeListener
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class WatchedDirectoryView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
WatchedDirectoryModel model
|
||||
|
||||
def dialog
|
||||
def p
|
||||
def mainFrame
|
||||
|
||||
def autoWatchCheckbox
|
||||
def syncIntervalField
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, "Watched Directory Configuration", true)
|
||||
dialog.setResizable(false)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label("Configuration for directory " + model.directory.directory.toString())
|
||||
}
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
gridBagLayout()
|
||||
label(text : "Auto-watch directory using operating system", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
autoWatchCheckbox = checkBox(selected : bind {model.autoWatch}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Directory sync frequency (seconds, 0 means never)", enabled : bind {!model.autoWatch}, constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
syncIntervalField = textField(text : bind {model.syncInterval}, columns: 4, enabled : bind {!model.autoWatch},
|
||||
constraints: gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END, insets : [0,10,0,0]))
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Save", saveAction)
|
||||
button(text : "Cancel", cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
autoWatchCheckbox.addChangeListener({e ->
|
||||
model.autoWatch = autoWatchCheckbox.model.isSelected()
|
||||
} as ChangeListener)
|
||||
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class WizardView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
WizardModel model
|
||||
|
||||
def dialog
|
||||
def p
|
||||
|
||||
void initUI() {
|
||||
dialog = new JDialog(model.parent, "Setup Wizard", true)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (id : "cards-panel", constraints : BorderLayout.CENTER) {
|
||||
cardLayout()
|
||||
model.steps.each {
|
||||
it.buildUI(builder)
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
gridLayout(rows:1, cols:2)
|
||||
panel {
|
||||
button(text : "Cancel", cancelAction)
|
||||
}
|
||||
panel {
|
||||
button(text : "Previous", enabled : bind {model.previousButtonEnabled}, previousAction)
|
||||
button(text : "Next", enabled : bind {model.nextButtonEnabled}, nextAction)
|
||||
button(text : "Finish", enabled : bind {model.finishButtonEnabled}, finishAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateLayout() {
|
||||
model.previousButtonEnabled = model.currentStep > 0
|
||||
model.nextButtonEnabled = model.steps.size() > (model.currentStep + 1)
|
||||
model.finishButtonEnabled = model.steps.size() == (model.currentStep + 1)
|
||||
|
||||
String constraints = model.steps[model.currentStep].getConstraint()
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel, constraints)
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(model.parent)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener( new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
void hide() {
|
||||
dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@@ -6,8 +6,8 @@ class DownloaderComparator implements Comparator<Downloader>{
|
||||
|
||||
@Override
|
||||
public int compare(Downloader o1, Downloader o2) {
|
||||
double d1 = o1.donePieces() * 1.0 / o1.nPieces
|
||||
double d2 = o2.donePieces() * 1.0 / o2.nPieces
|
||||
double d1 = o1.donePieces().toDouble() / o1.nPieces
|
||||
double d2 = o2.donePieces().toDouble() / o2.nPieces
|
||||
return Double.compare(d1, d2);
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,24 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.swing.SwingGriffonApplication
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogManager
|
||||
|
||||
import griffon.swing.SwingGriffonApplication
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class Launcher {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (System.getProperty("java.util.logging.config.file") == null) {
|
||||
log.info("No config file specified, so turning off most logging")
|
||||
def names = LogManager.getLogManager().getLoggerNames()
|
||||
while(names.hasMoreElements()) {
|
||||
def name = names.nextElement()
|
||||
LogManager.getLogManager().getLogger(name).setLevel(Level.SEVERE)
|
||||
}
|
||||
}
|
||||
|
||||
SwingGriffonApplication.main(args)
|
||||
}
|
||||
}
|
||||
|
@@ -2,13 +2,16 @@ package com.muwire.gui
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import java.awt.Font
|
||||
|
||||
class UISettings {
|
||||
|
||||
String lnf
|
||||
boolean showMonitor
|
||||
String font
|
||||
boolean autoFontSize
|
||||
int fontSize
|
||||
int fontSize, fontStyle
|
||||
int mainFrameX, mainFrameY
|
||||
boolean clearCancelledDownloads
|
||||
boolean clearFinishedDownloads
|
||||
boolean excludeLocalResult
|
||||
@@ -33,6 +36,7 @@ class UISettings {
|
||||
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true"))
|
||||
autoFontSize = Boolean.parseBoolean(props.getProperty("autoFontSize","false"))
|
||||
fontSize = Integer.parseInt(props.getProperty("fontSize","12"))
|
||||
fontStyle = Integer.parseInt(props.getProperty("fontStyle", String.valueOf(Font.PLAIN)))
|
||||
closeWarning = Boolean.parseBoolean(props.getProperty("closeWarning","true"))
|
||||
certificateWarning = Boolean.parseBoolean(props.getProperty("certificateWarning","true"))
|
||||
exitOnClose = Boolean.parseBoolean(props.getProperty("exitOnClose","false"))
|
||||
@@ -41,6 +45,9 @@ class UISettings {
|
||||
groupByFile = Boolean.parseBoolean(props.getProperty("groupByFile","false"))
|
||||
maxChatLines = Integer.parseInt(props.getProperty("maxChatLines","-1"))
|
||||
|
||||
mainFrameX = Integer.parseInt(props.getProperty("mainFrameX","1024"))
|
||||
mainFrameY = Integer.parseInt(props.getProperty("mainFrameY","768"))
|
||||
|
||||
searchHistory = DataUtil.readEncodedSet(props, "searchHistory")
|
||||
openTabs = DataUtil.readEncodedSet(props, "openTabs")
|
||||
}
|
||||
@@ -62,8 +69,12 @@ class UISettings {
|
||||
props.setProperty("storeSearchHistory", String.valueOf(storeSearchHistory))
|
||||
props.setProperty("groupByFile", String.valueOf(groupByFile))
|
||||
props.setProperty("maxChatLines", String.valueOf(maxChatLines))
|
||||
props.setProperty("fontStyle", String.valueOf(fontStyle))
|
||||
if (font != null)
|
||||
props.setProperty("font", font)
|
||||
|
||||
props.setProperty("mainFrameX", String.valueOf(mainFrameX))
|
||||
props.setProperty("mainFrameY", String.valueOf(mainFrameY))
|
||||
|
||||
DataUtil.writeEncodedSet(searchHistory, "searchHistory", props)
|
||||
DataUtil.writeEncodedSet(openTabs, "openTabs", props)
|
||||
|
@@ -0,0 +1,90 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import java.awt.GridBagConstraints
|
||||
|
||||
import javax.swing.JFileChooser
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
class DirectoriesStep extends WizardStep {
|
||||
|
||||
def downloadLocationField
|
||||
def incompleteLocationField
|
||||
def downloadLocationButton
|
||||
def incompleteLocationButton
|
||||
|
||||
public DirectoriesStep(WizardDefaults defaults) {
|
||||
super("directories", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
|
||||
builder.panel(constraints : getConstraint()) {
|
||||
gridBagLayout()
|
||||
label(text : "Select directories for saving downloaded and incomplete files.",
|
||||
constraints : gbc(gridx: 0, gridy: 0, gridwidth : 2, insets: [10,0,0,0]))
|
||||
label(text : "They will be created if they do not already exist.",
|
||||
constraints : gbc(gridx:0, gridy: 1, gridwidth: 2, insets: [0,0,10,0]))
|
||||
|
||||
label(text : "Directory for saving downloaded files", constraints : gbc(gridx:0, gridy: 2))
|
||||
downloadLocationField = textField(text : defaults.downloadLocation,
|
||||
constraints : gbc(gridx : 0, gridy : 3, fill : GridBagConstraints.HORIZONTAL, weightx: 100))
|
||||
downloadLocationButton = button(text : "Choose", constraints : gbc(gridx: 1, gridy: 3), actionPerformed : showDownloadChooser)
|
||||
label(text : "Directory for storing incomplete files", constraints : gbc(gridx:0, gridy: 4))
|
||||
incompleteLocationField = textField(text : defaults.incompleteLocation,
|
||||
constraints : gbc(gridx:0, gridy:5, fill : GridBagConstraints.HORIZONTAL, weightx: 100))
|
||||
incompleteLocationButton = button(text : "Choose", constraints : gbc(gridx: 1, gridy: 5), actionPerformed : showIncompleteChooser)
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> validate() {
|
||||
def rv = []
|
||||
if (!canWrite(downloadLocationField.text))
|
||||
rv << "Download location not writeable"
|
||||
if (!canWrite(incompleteLocationField.text))
|
||||
rv << "Incomplete location not writeable"
|
||||
rv
|
||||
}
|
||||
|
||||
private static boolean canWrite(String location) {
|
||||
File f = new File(location)
|
||||
if (f.exists())
|
||||
return f.isDirectory() && f.canWrite()
|
||||
canWrite(f.getParentFile().getAbsolutePath())
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
|
||||
muSettings.downloadLocation = new File(downloadLocationField.text)
|
||||
muSettings.incompleteLocation = new File(incompleteLocationField.text)
|
||||
|
||||
muSettings.downloadLocation.mkdirs()
|
||||
muSettings.incompleteLocation.mkdirs()
|
||||
}
|
||||
|
||||
def showDownloadChooser = {
|
||||
String text = chooseFile("Select directory for downloaded files")
|
||||
if (text != null)
|
||||
downloadLocationField.text = text
|
||||
}
|
||||
|
||||
def showIncompleteChooser = {
|
||||
String text = chooseFile("Select directory for incomplete files")
|
||||
if (text != null)
|
||||
incompleteLocationField.text = text
|
||||
}
|
||||
|
||||
private String chooseFile(String title) {
|
||||
def chooser = new JFileChooser()
|
||||
chooser.setFileHidingEnabled(false)
|
||||
chooser.setDialogTitle(title)
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
|
||||
int rv = chooser.showOpenDialog(null)
|
||||
if (rv == JFileChooser.APPROVE_OPTION)
|
||||
return chooser.getSelectedFile().getAbsolutePath()
|
||||
else
|
||||
return null
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import java.awt.GridBagConstraints
|
||||
|
||||
import javax.swing.border.TitledBorder
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
class EmbeddedRouterStep extends WizardStep {
|
||||
|
||||
|
||||
def udpPortField
|
||||
def tcpPortField
|
||||
|
||||
def inBwField
|
||||
def outBwField
|
||||
|
||||
public EmbeddedRouterStep(WizardDefaults defaults) {
|
||||
super("router", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
builder.panel(constraints : getConstraint()) {
|
||||
gridBagLayout()
|
||||
panel(border : titledBorder(title : "Port Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
constraints : gbc(gridx: 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100))) {
|
||||
gridBagLayout()
|
||||
label(text : "TCP port", constraints : gbc(gridx :0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
tcpPortField = textField(text : String.valueOf(defaults.i2npTcpPort), columns : 4, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "UDP port", constraints : gbc(gridx :0, gridy: 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
udpPortField = textField(text : String.valueOf(defaults.i2npUdpPort), columns : 4, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel( border : titledBorder(title : "Bandwidth Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100)) {
|
||||
gridBagLayout()
|
||||
label(text : "Inbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
inBwField = textField(text : String.valueOf(defaults.inBw), columns : 3, constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Outbound bandwidth (KB)", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
outBwField = textField(text : String.valueOf(defaults.outBw), columns : 3, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel (constraints : gbc(gridx: 0, gridy : 2, weighty: 100))
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> validate() {
|
||||
def rv = []
|
||||
try {
|
||||
int udpPort = Integer.parseInt(udpPortField.text)
|
||||
int tcpPort = Integer.parseInt(tcpPortField.text)
|
||||
if (udpPort <= 0 || udpPort > 0xFFFF)
|
||||
rv << "Invalid UDP Port"
|
||||
if (tcpPort <= 0 || tcpPort > 0xFFFF)
|
||||
rv << "Invalid TCP Port"
|
||||
} catch (NumberFormatException e) {
|
||||
rv << "Invalid port"
|
||||
}
|
||||
try {
|
||||
int outBw = Integer.parseInt(outBwField.text)
|
||||
int inBw = Integer.parseInt(inBwField.text)
|
||||
if (outBw <= 0)
|
||||
rv << "Out bandwidth cannot be negative"
|
||||
if (inBw <= 0)
|
||||
rv << "In bandwidth cannot be ngative"
|
||||
} catch (NumberFormatException e) {
|
||||
rv << "Invalid bandwidth"
|
||||
}
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
|
||||
i2pSettings['i2np.ntcp.port'] = tcpPortField.text
|
||||
i2pSettings['i2np.udp.port'] = udpPortField.text
|
||||
muSettings.outBw = Integer.parseInt(outBwField.text)
|
||||
muSettings.inBw = Integer.parseInt(inBwField.text)
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import java.awt.GridBagConstraints
|
||||
|
||||
import javax.swing.border.TitledBorder
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
class ExternalRouterStep extends WizardStep {
|
||||
|
||||
def addressField
|
||||
def portField
|
||||
|
||||
public ExternalRouterStep(WizardDefaults defaults) {
|
||||
super("router", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
builder.panel(constraints : getConstraint()) {
|
||||
gridBagLayout()
|
||||
panel(border : titledBorder(title : "External Router I2CP Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
constraints : gbc(gridx: 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100))) {
|
||||
gridBagLayout()
|
||||
|
||||
label(text : "Host", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
addressField = textField(text : defaults.i2cpHost, constraints : gbc(gridx:1, gridy:0, anchor: GridBagConstraints.LINE_END))
|
||||
|
||||
label(text : "Port", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
portField = textField(text : String.valueOf(defaults.i2cpPort), constraints : gbc(gridx:1, gridy:1, anchor: GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel(constraints : gbc(gridx:0, gridy:1, weighty: 100))
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> validate() {
|
||||
def rv = []
|
||||
try {
|
||||
InetAddress.getAllByName(addressField.text)
|
||||
} catch (UnknownHostException iox) {
|
||||
rv << "Not a valid InetAddress"
|
||||
}
|
||||
try {
|
||||
int port = Integer.parseInt(portField.text)
|
||||
if (port <= 0 && port > 0xFFFF)
|
||||
rv << "Not a valid port"
|
||||
} catch (NumberFormatException e) {
|
||||
rv << "Not a valid port"
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
|
||||
i2pSettings['i2cp.tcp.host'] = addressField.text
|
||||
i2pSettings['i2cp.tcp.port'] = portField.text
|
||||
}
|
||||
}
|
32
gui/src/main/groovy/com/muwire/gui/wizard/LastStep.groovy
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
class LastStep extends WizardStep {
|
||||
|
||||
private final boolean embeddedRouterAvailable
|
||||
|
||||
public LastStep(boolean embeddedRouterAvailable) {
|
||||
super("last", null)
|
||||
this.embeddedRouterAvailable = embeddedRouterAvailable
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
builder.panel(constraints: getConstraint()) {
|
||||
gridBagLayout()
|
||||
label(text: "The wizard is complete. Press \"Finish\" to launch MuWire.", constraints : gbc(gridx: 0, gridy: 0))
|
||||
if (embeddedRouterAvailable)
|
||||
label(text : "MuWire will launch an embedded I2P router. This can take a few minutes.", constraints: gbc(gridx:0, gridy:1))
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> validate() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
class NicknameStep extends WizardStep {
|
||||
|
||||
volatile def nickField
|
||||
|
||||
public NicknameStep() {
|
||||
super("nickname", null)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
builder.panel(constraints : getConstraint()) {
|
||||
label(text: "Select a nickname")
|
||||
nickField = textField(columns: 30)
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> validate() {
|
||||
String nickname = nickField.text
|
||||
if (nickname == null)
|
||||
return ['Please select a nickname']
|
||||
nickname = nickname.trim()
|
||||
if (nickname.length() == 0)
|
||||
return ['Nickname cannot be blank']
|
||||
if (!DataUtil.isValidName(nickname))
|
||||
return ["Nickname cannot contain any of ${Constants.INVALID_NICKNAME_CHARS} and must be no longer than ${Constants.MAX_NICKNAME_LENGTH} characters. Choose another."]
|
||||
null
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
|
||||
muSettings.nickname = nickField.text.trim()
|
||||
}
|
||||
}
|
61
gui/src/main/groovy/com/muwire/gui/wizard/TunnelStep.groovy
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import java.awt.GridBagConstraints
|
||||
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.border.TitledBorder
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
class TunnelStep extends WizardStep {
|
||||
|
||||
|
||||
def tunnelLengthSlider
|
||||
def tunnelQuantitySlider
|
||||
|
||||
public TunnelStep(WizardDefaults defaults) {
|
||||
super("tunnels", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
builder.panel (constraints : getConstraint()) {
|
||||
gridBagLayout()
|
||||
panel (border : titledBorder(title : "Speed vs. Anonymity", border : etchedBorder(), titlePosition: TitledBorder.TOP,
|
||||
constraints : gbc(gridx: 0, gridy: 0, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
|
||||
def lengthTable = new Hashtable()
|
||||
lengthTable.put(1, new JLabel("Max Speed"))
|
||||
lengthTable.put(3, new JLabel("Max Anonymity"))
|
||||
tunnelLengthSlider = slider(minimum : 1, maximum : 3, value : defaults.tunnelLength,
|
||||
majorTickSpacing : 1, snapToTicks: true, paintTicks: true, labelTable : lengthTable,
|
||||
paintLabels : true)
|
||||
}
|
||||
panel (border : titledBorder(title : "Reliability vs. Resource Usage", border : etchedBorder(), titlePosition: TitledBorder.TOP,
|
||||
constraints : gbc(gridx: 0, gridy: 1, fill : GridBagConstraints.HORIZONTAL, weightx : 100))) {
|
||||
def quantityTable = new Hashtable()
|
||||
quantityTable.put(1, new JLabel("Min Resources"))
|
||||
quantityTable.put(6, new JLabel("Max Reliability"))
|
||||
tunnelQuantitySlider = slider(minimum : 1, maximum : 6, value : defaults.tunnelQuantity,
|
||||
majorTickSpacing : 1, snapToTicks : true, paintTicks: true, labelTable : quantityTable,
|
||||
paintLabels : true)
|
||||
}
|
||||
panel(constraints : gbc(gridx:0, gridy: 2, weighty: 100))
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> validate() {
|
||||
return null
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply(MuWireSettings muSettings, Properties i2pSettings) {
|
||||
String tunnelLength = String.valueOf(tunnelLengthSlider.value)
|
||||
i2pSettings['inbound.length'] = tunnelLength
|
||||
i2pSettings['outbound.length'] = tunnelLength
|
||||
|
||||
String tunnelQuantity = tunnelQuantitySlider.value
|
||||
i2pSettings['inbound.quantity'] = tunnelQuantity
|
||||
i2pSettings['outbound.quantity'] = tunnelQuantity
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
class WizardDefaults {
|
||||
|
||||
String downloadLocation
|
||||
String incompleteLocation
|
||||
String i2cpHost
|
||||
int i2cpPort
|
||||
int i2npTcpPort
|
||||
int i2npUdpPort
|
||||
int inBw, outBw
|
||||
int tunnelLength, tunnelQuantity
|
||||
|
||||
WizardDefaults() {
|
||||
this(new Properties())
|
||||
}
|
||||
|
||||
WizardDefaults(Properties props) {
|
||||
downloadLocation = props.getProperty("downloadLocation", getDefaultPath("MuWire Downloads"))
|
||||
incompleteLocation = props.getProperty("incompleteLocation", getDefaultPath("MuWire Incompletes"))
|
||||
i2cpHost = props.getProperty("i2cpHost","127.0.0.1")
|
||||
i2cpPort = Integer.parseInt(props.getProperty("i2cpPort","7654"))
|
||||
|
||||
Random r = new Random()
|
||||
int randomPort = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
|
||||
|
||||
i2npTcpPort = Integer.parseInt(props.getProperty("i2npTcpPort", String.valueOf(randomPort)))
|
||||
i2npUdpPort = Integer.parseInt(props.getProperty("i2npUdpPort", String.valueOf(randomPort)))
|
||||
|
||||
inBw = Integer.parseInt(props.getProperty("inBw","512"))
|
||||
outBw = Integer.parseInt(props.getProperty("outBw","256"))
|
||||
|
||||
tunnelLength = Integer.parseInt(props.getProperty("tunnelLength","3"))
|
||||
tunnelQuantity = Integer.parseInt(props.getProperty("tunnelQuantity","4"))
|
||||
}
|
||||
|
||||
private static String getDefaultPath(String pathName) {
|
||||
File f = new File(System.getProperty("user.home"), pathName)
|
||||
f.getAbsolutePath()
|
||||
}
|
||||
}
|
24
gui/src/main/groovy/com/muwire/gui/wizard/WizardStep.groovy
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.muwire.gui.wizard
|
||||
|
||||
import com.muwire.core.MuWireSettings
|
||||
|
||||
abstract class WizardStep {
|
||||
|
||||
final String constraint
|
||||
final WizardDefaults defaults
|
||||
|
||||
protected WizardStep(String constraint, WizardDefaults defaults) {
|
||||
this.constraint = constraint
|
||||
this.defaults = defaults
|
||||
}
|
||||
|
||||
|
||||
protected abstract void buildUI(FactoryBuilderSupport builder)
|
||||
|
||||
/**
|
||||
* @return list of errors, null if validation is successful
|
||||
*/
|
||||
protected abstract List<String> validate()
|
||||
|
||||
protected abstract void apply(MuWireSettings muSettings, Properties i2pSettings)
|
||||
}
|
@@ -6,4 +6,5 @@ dependencies {
|
||||
compile "net.i2p:i2p:${i2pVersion}"
|
||||
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
|
||||
}
|
||||
|
62
host-cache/logging/logging.properties
Normal file
@@ -0,0 +1,62 @@
|
||||
############################################################
|
||||
# Default Logging Configuration File
|
||||
#
|
||||
# You can use a different file by specifying a filename
|
||||
# with the java.util.logging.config.file system property.
|
||||
# For example java -Djava.util.logging.config.file=myfile
|
||||
############################################################
|
||||
|
||||
############################################################
|
||||
# Global properties
|
||||
############################################################
|
||||
|
||||
# "handlers" specifies a comma separated list of log Handler
|
||||
# classes. These handlers will be installed during VM startup.
|
||||
# Note that these classes must be on the system classpath.
|
||||
# By default we only configure a ConsoleHandler, which will only
|
||||
# show messages at the INFO and above levels.
|
||||
handlers= java.util.logging.FileHandler
|
||||
|
||||
# To also add the FileHandler, use the following line instead.
|
||||
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler
|
||||
|
||||
# Default global logging level.
|
||||
# This specifies which kinds of events are logged across
|
||||
# all loggers. For any given facility this global level
|
||||
# can be overriden by a facility specific level
|
||||
# Note that the ConsoleHandler also has a separate level
|
||||
# setting to limit messages printed to the console.
|
||||
.level= INFO
|
||||
|
||||
############################################################
|
||||
# Handler specific properties.
|
||||
# Describes specific configuration info for Handlers.
|
||||
############################################################
|
||||
|
||||
# default file output is in user's home directory.
|
||||
java.util.logging.FileHandler.pattern = hostcache.log
|
||||
java.util.logging.FileHandler.limit = 5000000
|
||||
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
|
||||
net.i2p.client.streaming.impl.level = SEVERE
|
23
host-cache/scripts/count_total.py
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os,sys,json
|
||||
|
||||
if len(sys.argv) < 2 :
|
||||
print("This script counts unique hosts in the MuWire network",file = sys.stderr)
|
||||
print("Pass the prefix of the files to analyse. For example:",file = sys.stderr)
|
||||
print("\"20200427\" will count unique hosts on 27th of April 2020",file = sys.stderr)
|
||||
print("\"202004\" will count unique hosts during all of April 2020",file = sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
day = sys.argv[1]
|
||||
files = os.listdir(".")
|
||||
files = [x for x in files if x.startswith(day)]
|
||||
|
||||
hosts = set()
|
||||
|
||||
for f in files:
|
||||
for line in open(f):
|
||||
host = json.loads(line)
|
||||
hosts.add(host["destination"])
|
||||
|
||||
print(len(hosts))
|
@@ -40,12 +40,13 @@ class Crawler {
|
||||
try {
|
||||
uuid = UUID.fromString(pong.uuid)
|
||||
} catch (IllegalArgumentException bad) {
|
||||
log.log(Level.WARNING,"couldn't parse uuid",bad)
|
||||
hostPool.fail(host)
|
||||
return
|
||||
}
|
||||
|
||||
if (!uuid.equals(currentUUID)) {
|
||||
log.info("uuid mismatch")
|
||||
log.warning("uuid mismatch $uuid expected $currentUUID")
|
||||
hostPool.fail(host)
|
||||
return
|
||||
}
|
||||
@@ -75,11 +76,12 @@ class Crawler {
|
||||
}
|
||||
|
||||
synchronized def startCrawl() {
|
||||
currentUUID = UUID.randomUUID()
|
||||
log.info("starting new crawl with uuid $currentUUID inFlight ${inFlight.size()}")
|
||||
if (!inFlight.isEmpty()) {
|
||||
inFlight.values().each { hostPool.fail(it) }
|
||||
inFlight.clear()
|
||||
}
|
||||
currentUUID = UUID.randomUUID()
|
||||
hostPool.getUnverified(parallel).each {
|
||||
inFlight.put(it.destination, it)
|
||||
pinger.ping(it, currentUUID)
|
||||
|
@@ -15,4 +15,9 @@ class Host {
|
||||
public boolean equals(other) {
|
||||
return destination.equals(other.destination)
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
"Host[b32:${destination.toBase32()} verifyTime:$verifyTime verificationFailures:$verificationFailures]"
|
||||
}
|
||||
}
|
||||
|
@@ -51,8 +51,13 @@ public class HostCache {
|
||||
println myDest.toBase64()
|
||||
}
|
||||
|
||||
def props = System.getProperties().clone()
|
||||
props.putAt("inbound.nickname", "MuWire HostCache")
|
||||
Properties props = System.getProperties().clone()
|
||||
props["inbound.nickname"] = "MuWire HostCache"
|
||||
|
||||
def i2pPropsFile = new File(home,"i2p.properties")
|
||||
if (i2pPropsFile.exists()) {
|
||||
i2pPropsFile.withInputStream { props.load(it) }
|
||||
}
|
||||
session = i2pClient.createSession(new FileInputStream(keyfile), props)
|
||||
myDest = session.getMyDestination()
|
||||
|
||||
@@ -64,8 +69,10 @@ public class HostCache {
|
||||
Timer timer = new Timer("timer", true)
|
||||
timer.schedule({hostPool.age()} as TimerTask, 1000,1000)
|
||||
timer.schedule({crawler.startCrawl()} as TimerTask, 10000, 10000)
|
||||
File verified = new File("verified.json")
|
||||
File unverified = new File("unverified.json")
|
||||
File verified = new File("verified")
|
||||
File unverified = new File("unverified")
|
||||
verified.mkdir()
|
||||
unverified.mkdir()
|
||||
timer.schedule({hostPool.serialize(verified, unverified)} as TimerTask, 10000, 60 * 60 * 1000)
|
||||
|
||||
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
|
||||
|
@@ -1,11 +1,14 @@
|
||||
package com.muwire.hostcache
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
class HostPool {
|
||||
|
||||
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMdd-HH")
|
||||
|
||||
final def maxFailures
|
||||
final def maxAge
|
||||
|
||||
@@ -77,9 +80,11 @@ class HostPool {
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void serialize(File verifiedFile, File unverifiedFile) {
|
||||
write(verifiedFile, verified.values())
|
||||
write(unverifiedFile, unverified.values())
|
||||
synchronized void serialize(File verifiedPath, File unverifiedPath) {
|
||||
def now = new Date()
|
||||
now = SDF.format(now)
|
||||
write(new File(verifiedPath, now), verified.values())
|
||||
write(new File(unverifiedPath, now), unverified.values())
|
||||
}
|
||||
|
||||
private void write(File target, Collection hosts) {
|
||||
|