Compare commits
89 Commits
setup-wiza
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bcb41baca2 | ||
![]() |
72985bacb6 | ||
![]() |
3387d22a6c | ||
![]() |
10bd566d58 | ||
![]() |
f4e0c707df | ||
![]() |
c11a427483 | ||
![]() |
e9db22c562 | ||
![]() |
fa53a35023 | ||
![]() |
94dd6101aa | ||
![]() |
e65fbe1bd1 | ||
![]() |
964e315367 | ||
![]() |
140231e362 | ||
![]() |
c73a821c67 | ||
![]() |
0ebe00b526 | ||
![]() |
b2a3bfce54 | ||
![]() |
c490a511bd | ||
![]() |
cbaa3470d2 | ||
![]() |
84d61fccd5 | ||
![]() |
a88db8f50f | ||
![]() |
5a38154e15 | ||
![]() |
e5891de136 | ||
![]() |
1e729bae1c | ||
![]() |
3e6e0c7e9f | ||
![]() |
44af23c162 | ||
![]() |
a262c99efe | ||
![]() |
9eff723dd3 | ||
![]() |
f2531c80d5 | ||
![]() |
944cb29901 | ||
![]() |
2c7cf24942 | ||
![]() |
5a94d14b8e | ||
![]() |
f20b23434f | ||
![]() |
8d523a6265 | ||
![]() |
38b9ab5200 | ||
![]() |
f6fdf9e33f | ||
![]() |
b729a89672 | ||
![]() |
e531093b28 | ||
![]() |
b18772465c | ||
![]() |
20aac03789 | ||
![]() |
ac8d9c1281 | ||
![]() |
ad8693d512 | ||
![]() |
144ad634c8 | ||
![]() |
4cdb383b9f | ||
![]() |
9c6f6bf266 | ||
![]() |
9a4e6b868b | ||
![]() |
31e0962b73 | ||
![]() |
2acac4b1ea | ||
![]() |
03d00a22d7 | ||
![]() |
0e54fb1ed1 | ||
![]() |
2dee5e2a8a | ||
![]() |
c7406a4838 | ||
![]() |
c9eb702d7c | ||
![]() |
253603cac7 | ||
![]() |
3af6ee3bce | ||
![]() |
bfa88b0b7a | ||
![]() |
1400967b22 | ||
![]() |
5739760075 | ||
![]() |
fec042ec36 | ||
![]() |
d3477b91fc | ||
![]() |
1f973cf076 | ||
![]() |
fdb64f5539 | ||
![]() |
5b4f3202d6 | ||
![]() |
64eb2dad80 | ||
![]() |
ffe328eee6 | ||
![]() |
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 |
@@ -4,7 +4,7 @@ The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and is
|
||||
|
||||
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.4 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.
|
||||
|
||||
@@ -21,7 +21,7 @@ If you want to run the unit tests, type
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
|
||||
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project. If you want to package MuWire for a Linux distribution, see the [Packaging] wiki page.
|
||||
|
||||
## Running the GUI
|
||||
|
||||
@@ -30,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
|
||||
|
||||
@@ -70,6 +68,7 @@ You can find the full key at https://keybase.io/zlatinb
|
||||
[Wiki]: https://github.com/zlatinb/muwire/wiki
|
||||
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
|
||||
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
|
||||
[Packaging]: https://github.com/zlatinb/muwire/wiki/Packaging
|
||||
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
|
||||
[I2P Github]: https://github.com/i2p/i2p.i2p
|
||||
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
|
||||
|
13
TODO.md
13
TODO.md
@@ -15,17 +15,14 @@ This helps with scalability
|
||||
* Metadata parsing and search
|
||||
* Automatic adjustment of number of I2P tunnels
|
||||
* Persist trust immediately
|
||||
* Check if user-selected download and incomplete locations exist and are writeable
|
||||
* Enum i18n
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* Advertise file feed and browseability in upload headers
|
||||
* Manual polling / shared folder re-scan (because polling NAS doesn't work)
|
||||
* Download queue with priorities
|
||||
* Use tracker pings - either embedded logic or external mwtrackerd to add more sources to downloads
|
||||
|
||||
### Chat
|
||||
* echo "unknown/innappropriate command" in the console
|
||||
* break up lines on CR/LF, send multiple messages
|
||||
* Style timestamps and persona names
|
||||
* enforce # in room names or ignore it
|
||||
* auto-create/join channel on server start
|
||||
* jump from notification window to room with message
|
||||
@@ -34,7 +31,6 @@ This helps with scalability
|
||||
* 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
|
||||
@@ -43,4 +39,7 @@ This helps with scalability
|
||||
* Upload files from browser to plugin via drag-and-drop
|
||||
* Check permissions, display better errors when sharing local folders
|
||||
|
||||
|
||||
### mwtrackerd
|
||||
* `save` and `load` JSON-RPC commands that save and load swarm state respectively
|
||||
* load-test with many many hashes (1M?)
|
||||
* evaluate other usage scenarios besides website backend
|
||||
|
@@ -2,13 +2,13 @@ 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
|
||||
|
@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.7.0"
|
||||
private static final String MW_VERSION = "0.7.4"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@@ -7,12 +7,12 @@ plugins {
|
||||
dependencies {
|
||||
api "net.i2p:i2p:${i2pVersion}"
|
||||
api "net.i2p:router:${i2pVersion}"
|
||||
implementation "net.i2p.client:mstreaming:${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:2.4.15'
|
||||
testImplementation 'org.codehaus.groovy:groovy-all:3.0.4'
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ configurations {
|
||||
}
|
||||
}
|
||||
|
||||
configurations.testImplementation {
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-testng'
|
||||
}
|
||||
|
||||
// publish core to local maven repo for sister projects
|
||||
publishing {
|
||||
publications {
|
||||
|
@@ -23,8 +23,10 @@ import com.muwire.core.connection.I2PAcceptor
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.connection.LeafConnectionManager
|
||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||
import com.muwire.core.download.DownloadHopelessEvent
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.download.SourceVerifiedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.download.UIDownloadPausedEvent
|
||||
@@ -70,6 +72,7 @@ import com.muwire.core.hostcache.HostDiscoveredEvent
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
import com.muwire.core.search.BrowseManager
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.ResponderCache
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.ResultsSender
|
||||
import com.muwire.core.search.SearchEvent
|
||||
@@ -114,6 +117,7 @@ public class Core {
|
||||
final MuWireSettings muOptions
|
||||
|
||||
final I2PSession i2pSession;
|
||||
private I2PSocketManager i2pSocketManager
|
||||
final TrustService trustService
|
||||
final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
@@ -220,14 +224,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)
|
||||
@@ -284,6 +287,7 @@ public class Core {
|
||||
log.info("initializing mesh manager")
|
||||
MeshManager meshManager = new MeshManager(fileManager, home, props)
|
||||
eventBus.register(SourceDiscoveredEvent.class, meshManager)
|
||||
eventBus.register(SourceVerifiedEvent.class, meshManager)
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
@@ -306,10 +310,17 @@ public class Core {
|
||||
eventBus.register(HostDiscoveredEvent.class, hostCache)
|
||||
eventBus.register(ConnectionEvent.class, hostCache)
|
||||
|
||||
|
||||
log.info("initializing responder cache")
|
||||
ResponderCache responderCache = new ResponderCache(props.responderCacheSize)
|
||||
eventBus.register(UIResultBatchEvent.class, responderCache)
|
||||
eventBus.register(SourceVerifiedEvent.class, responderCache)
|
||||
|
||||
|
||||
log.info("initializing connection manager")
|
||||
connectionManager = props.isLeaf() ?
|
||||
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
|
||||
new UltrapeerConnectionManager(eventBus, me, 512, 512, hostCache, trustService, props)
|
||||
new UltrapeerConnectionManager(eventBus, me, props.peerConnections, props.leafConnections, hostCache, responderCache, trustService, props)
|
||||
eventBus.register(TrustEvent.class, connectionManager)
|
||||
eventBus.register(ConnectionEvent.class, connectionManager)
|
||||
eventBus.register(DisconnectionEvent.class, connectionManager)
|
||||
@@ -318,16 +329,16 @@ public class Core {
|
||||
log.info("initializing cache client")
|
||||
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
|
||||
|
||||
if (!props.plugin) {
|
||||
if (!(props.plugin || props.disableUpdates)) {
|
||||
log.info("initializing update client")
|
||||
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
|
||||
eventBus.register(FileDownloadedEvent.class, updateClient)
|
||||
eventBus.register(UIResultBatchEvent.class, updateClient)
|
||||
} else
|
||||
log.info("running as plugin, not initializing update client")
|
||||
log.info("running as plugin or updates disabled, 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)
|
||||
@@ -362,8 +373,17 @@ public class Core {
|
||||
eventBus.register(QueryEvent.class, searchManager)
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info("initializing chat manager")
|
||||
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
|
||||
eventBus.with {
|
||||
register(UIConnectChatEvent.class, chatManager)
|
||||
register(UIDisconnectChatEvent.class, chatManager)
|
||||
register(ChatMessageEvent.class, chatManager)
|
||||
register(ChatDisconnectionEvent.class, chatManager)
|
||||
}
|
||||
|
||||
log.info("initializing download manager")
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me, chatServer)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadFeedItemEvent.class, downloadManager)
|
||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||
@@ -372,6 +392,7 @@ public class Core {
|
||||
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||
eventBus.register(DownloadHopelessEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
|
||||
@@ -382,18 +403,8 @@ public class Core {
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
|
||||
|
||||
log.info("initializing chat manager")
|
||||
chatManager = new ChatManager(eventBus, me, i2pConnector, trustService, props)
|
||||
eventBus.with {
|
||||
register(UIConnectChatEvent.class, chatManager)
|
||||
register(UIDisconnectChatEvent.class, chatManager)
|
||||
register(ChatMessageEvent.class, chatManager)
|
||||
register(ChatDisconnectionEvent.class, chatManager)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -501,8 +512,12 @@ public class Core {
|
||||
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)
|
||||
@@ -546,7 +561,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.7.0")
|
||||
Core core = new Core(props, home, "0.7.4")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@@ -16,7 +16,7 @@ class MuWireSettings {
|
||||
boolean allowTrustLists
|
||||
int trustListInterval
|
||||
Set<Persona> trustSubscriptions
|
||||
int downloadRetryInterval
|
||||
int downloadRetryInterval, downloadMaxFailures
|
||||
int totalUploadSlots
|
||||
int uploadSlotsPerUser
|
||||
int updateCheckInterval
|
||||
@@ -37,10 +37,14 @@ class MuWireSettings {
|
||||
boolean advertiseFeed
|
||||
boolean autoPublishSharedFiles
|
||||
boolean defaultFeedAutoDownload
|
||||
int defaultFeedUpdateInterval
|
||||
long defaultFeedUpdateInterval
|
||||
int defaultFeedItemsToKeep
|
||||
boolean defaultFeedSequential
|
||||
|
||||
int peerConnections
|
||||
int leafConnections
|
||||
|
||||
int responderCacheSize
|
||||
|
||||
boolean startChatServer
|
||||
int maxChatConnections
|
||||
@@ -48,11 +52,12 @@ class MuWireSettings {
|
||||
File chatWelcomeFile
|
||||
Set<String> watchedDirectories
|
||||
float downloadSequentialRatio
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval
|
||||
int hostClearInterval, hostHopelessInterval, hostRejectInterval, hostHopelessPurgeInterval
|
||||
int meshExpiration
|
||||
int speedSmoothSeconds
|
||||
boolean embeddedRouter
|
||||
boolean plugin
|
||||
boolean disableUpdates
|
||||
int inBw, outBw
|
||||
Set<String> watchedKeywords
|
||||
Set<String> watchedRegexes
|
||||
@@ -76,6 +81,7 @@ class MuWireSettings {
|
||||
if (incompleteLocationProp != null)
|
||||
incompleteLocation = new File(incompleteLocationProp)
|
||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
|
||||
downloadMaxFailures = Integer.parseInt(props.getProperty("downloadMaxFailures","10"))
|
||||
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
|
||||
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
|
||||
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
|
||||
@@ -84,11 +90,13 @@ class MuWireSettings {
|
||||
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
|
||||
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
|
||||
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
|
||||
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
|
||||
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "60"))
|
||||
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
|
||||
hostHopelessPurgeInterval = Integer.valueOf(props.getProperty("hostHopelessPurgeInterval","1440"))
|
||||
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
|
||||
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
|
||||
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
|
||||
disableUpdates = Boolean.valueOf(props.getProperty("disableUpdates","false"))
|
||||
inBw = Integer.valueOf(props.getProperty("inBw","256"))
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
@@ -102,9 +110,16 @@ 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", "60000"))
|
||||
defaultFeedUpdateInterval = Long.valueOf(props.getProperty("defaultFeedUpdateInterval", "3600000"))
|
||||
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
// ultrapeer connection settings
|
||||
leafConnections = Integer.valueOf(props.getProperty("leafConnections","512"))
|
||||
peerConnections = Integer.valueOf(props.getProperty("peerConnections","128"))
|
||||
|
||||
// responder cache settings
|
||||
responderCacheSize = Integer.valueOf(props.getProperty("responderCacheSize","32"))
|
||||
|
||||
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"))
|
||||
@@ -142,6 +157,7 @@ class MuWireSettings {
|
||||
if (incompleteLocation != null)
|
||||
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
|
||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||
props.setProperty("downloadMaxFailures", String.valueOf(downloadMaxFailures))
|
||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
|
||||
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
|
||||
@@ -152,9 +168,11 @@ class MuWireSettings {
|
||||
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
|
||||
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
|
||||
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
|
||||
props.setProperty("hostHopelessPurgeInterval", String.valueOf(hostHopelessPurgeInterval))
|
||||
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
|
||||
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
|
||||
props.setProperty("plugin", String.valueOf(plugin))
|
||||
props.setProperty("disableUpdates", String.valueOf(disableUpdates))
|
||||
props.setProperty("inBw", String.valueOf(inBw))
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
@@ -170,6 +188,13 @@ class MuWireSettings {
|
||||
props.setProperty("defaultFeedSequential", String.valueOf(defaultFeedSequential))
|
||||
props.setProperty("defaultFeedUpdateInterval", String.valueOf(defaultFeedUpdateInterval))
|
||||
|
||||
// ultrapeer connection settings
|
||||
props.setProperty("peerConnections", String.valueOf(peerConnections))
|
||||
props.setProperty("leafConnections", String.valueOf(leafConnections))
|
||||
|
||||
// responder cache settings
|
||||
props.setProperty("responderCacheSize", String.valueOf(responderCacheSize))
|
||||
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
|
@@ -29,6 +29,10 @@ class ChatManager {
|
||||
timer.schedule({connect()} as TimerTask, 1000, 1000)
|
||||
}
|
||||
|
||||
boolean isConnected(Persona p) {
|
||||
clients.containsKey(p)
|
||||
}
|
||||
|
||||
void onUIConnectChatEvent(UIConnectChatEvent e) {
|
||||
if (e.host == me) {
|
||||
eventBus.publish(new ChatConnectionEvent(status : ChatConnectionAttemptStatus.SUCCESSFUL,
|
||||
|
@@ -60,6 +60,10 @@ class ChatServer {
|
||||
echo(getWelcome(),me.destination)
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
running.get()
|
||||
}
|
||||
|
||||
private String getWelcome() {
|
||||
String welcome = DEFAULT_WELCOME
|
||||
if (settings.chatWelcomeFile != null)
|
||||
@@ -197,9 +201,19 @@ class ChatServer {
|
||||
return
|
||||
}
|
||||
|
||||
if ((command.action.console && e.room != CONSOLE) ||
|
||||
(!command.action.console && e.room == CONSOLE) ||
|
||||
!command.action.user)
|
||||
if (command.action.console && e.room != CONSOLE) {
|
||||
echo("/SAY ERROR: You can only execute that command in the chat console, not in a chat room.",
|
||||
e.sender.destination, e.room)
|
||||
return
|
||||
}
|
||||
|
||||
if (!command.action.console && e.room == CONSOLE) {
|
||||
echo("/SAY ERROR: You need to be in a chat room. Type /LIST for list of rooms or /JOIN to join or create a room.",
|
||||
e.sender.destination)
|
||||
return
|
||||
}
|
||||
|
||||
if (!command.action.user)
|
||||
return
|
||||
|
||||
if (command.action.local && e.sender != me)
|
||||
@@ -296,17 +310,17 @@ class ChatServer {
|
||||
echo(help, d)
|
||||
}
|
||||
|
||||
private void echo(String payload, Destination d) {
|
||||
private void echo(String payload, Destination d, String room = CONSOLE) {
|
||||
log.info "echoing $payload"
|
||||
UUID uuid = UUID.randomUUID()
|
||||
long now = System.currentTimeMillis()
|
||||
byte [] sig = ChatConnection.sign(uuid, now, CONSOLE, payload, me, me, spk)
|
||||
byte [] sig = ChatConnection.sign(uuid, now, room, payload, me, me, spk)
|
||||
ChatMessageEvent echo = new ChatMessageEvent(
|
||||
uuid : uuid,
|
||||
payload : payload,
|
||||
sender : me,
|
||||
host : me,
|
||||
room : CONSOLE,
|
||||
room : room,
|
||||
chatTime : now,
|
||||
sig : sig
|
||||
)
|
||||
|
@@ -49,6 +49,8 @@ abstract class Connection implements Closeable {
|
||||
protected final String name
|
||||
|
||||
long lastPingSentTime, lastPongReceivedTime
|
||||
|
||||
private volatile UUID lastPingUUID
|
||||
|
||||
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
|
||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||
@@ -132,6 +134,8 @@ abstract class Connection implements Closeable {
|
||||
def ping = [:]
|
||||
ping.type = "Ping"
|
||||
ping.version = 1
|
||||
lastPingUUID = UUID.randomUUID()
|
||||
ping.uuid = lastPingUUID.toString()
|
||||
messages.put(ping)
|
||||
lastPingSentTime = System.currentTimeMillis()
|
||||
}
|
||||
@@ -160,12 +164,14 @@ abstract class Connection implements Closeable {
|
||||
messages.put(query)
|
||||
}
|
||||
|
||||
protected void handlePing() {
|
||||
protected void handlePing(def ping) {
|
||||
log.fine("$name received ping")
|
||||
def pong = [:]
|
||||
pong.type = "Pong"
|
||||
pong.version = 1
|
||||
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
|
||||
if (ping.uuid != null)
|
||||
pong.uuid = ping.uuid
|
||||
pong.pongs = hostCache.getGoodHosts(2).collect { d -> d.toBase64() }
|
||||
messages.put(pong)
|
||||
}
|
||||
|
||||
@@ -174,7 +180,23 @@ abstract class Connection implements Closeable {
|
||||
lastPongReceivedTime = System.currentTimeMillis()
|
||||
if (pong.pongs == null)
|
||||
throw new Exception("Pong doesn't have pongs")
|
||||
pong.pongs.each {
|
||||
|
||||
if (lastPingUUID == null) {
|
||||
log.fine "$name received an unexpected pong"
|
||||
return
|
||||
}
|
||||
if (pong.uuid == null) {
|
||||
log.fine "$name pong doesn't have uuid"
|
||||
return
|
||||
}
|
||||
UUID pongUUID = UUID.fromString(pong.uuid)
|
||||
if (pongUUID != lastPingUUID) {
|
||||
log.fine "$name ping/pong uuid mismatch"
|
||||
return
|
||||
}
|
||||
lastPingUUID = null
|
||||
|
||||
pong.pongs.stream().limit(2).forEach {
|
||||
def dest = new Destination(it)
|
||||
eventBus.publish(new HostDiscoveredEvent(destination: dest))
|
||||
}
|
||||
|
@@ -380,7 +380,7 @@ class ConnectionAcceptor {
|
||||
|
||||
os.write("Count: ${sharedFiles.size()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean chat = chatServer.running.get() && settings.advertiseChat
|
||||
boolean chat = chatServer.isRunning() && settings.advertiseChat
|
||||
os.write("Chat: ${chat}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
boolean feed = settings.fileFeed && settings.advertiseFeed
|
||||
|
@@ -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()
|
||||
|
@@ -53,7 +53,7 @@ class PeerConnection extends Connection {
|
||||
if (json.type == null)
|
||||
throw new Exception("missing json type")
|
||||
switch(json.type) {
|
||||
case "Ping" : handlePing(); break;
|
||||
case "Ping" : handlePing(json); break;
|
||||
case "Pong" : handlePong(json); break;
|
||||
case "Search": handleSearch(json); break
|
||||
default :
|
||||
|
@@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.ResponderCache
|
||||
import com.muwire.core.trust.TrustService
|
||||
|
||||
import groovy.util.logging.Log
|
||||
@@ -18,18 +19,22 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
|
||||
final int maxPeers, maxLeafs
|
||||
final TrustService trustService
|
||||
final ResponderCache responderCache
|
||||
|
||||
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
|
||||
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
|
||||
|
||||
private final Random random = new Random()
|
||||
|
||||
UltrapeerConnectionManager() {}
|
||||
|
||||
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
|
||||
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
|
||||
HostCache hostCache, ResponderCache responderCache, TrustService trustService, MuWireSettings settings) {
|
||||
super(eventBus, me, hostCache, settings)
|
||||
this.maxPeers = maxPeers
|
||||
this.maxLeafs = maxLeafs
|
||||
this.trustService = trustService
|
||||
this.responderCache = responderCache
|
||||
}
|
||||
@Override
|
||||
public void drop(Destination d) {
|
||||
@@ -44,8 +49,18 @@ class UltrapeerConnectionManager extends ConnectionManager {
|
||||
if (e.replyTo != me.destination && e.receivedOn != me.destination &&
|
||||
!leafConnections.containsKey(e.receivedOn))
|
||||
e.firstHop = false
|
||||
final int connCount = peerConnections.size()
|
||||
if (connCount == 0)
|
||||
return
|
||||
final int treshold = (int)(Math.sqrt(connCount)) + 1
|
||||
peerConnections.values().each {
|
||||
if (e.getReceivedOn() != it.getEndpoint().getDestination())
|
||||
// 1. do not send query back to originator
|
||||
// 2. if firstHop forward to everyone
|
||||
// 3. otherwise to everyone who has recently responded/transferred to us + randomized sqrt of neighbors
|
||||
if (e.getReceivedOn() != it.getEndpoint().getDestination() &&
|
||||
(e.firstHop ||
|
||||
responderCache.hasResponded(it.endpoint.destination) ||
|
||||
random.nextInt(connCount) < treshold))
|
||||
it.sendQuery(e)
|
||||
}
|
||||
}
|
||||
@@ -105,8 +120,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()
|
||||
}
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class DownloadHopelessEvent extends Event {
|
||||
Downloader downloader
|
||||
}
|
@@ -23,6 +23,8 @@ import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
import com.muwire.core.chat.ChatServer
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executor
|
||||
@@ -35,16 +37,17 @@ public class DownloadManager {
|
||||
private final EventBus eventBus
|
||||
private final TrustService trustService
|
||||
private final MeshManager meshManager
|
||||
private final MuWireSettings muSettings
|
||||
final MuWireSettings muSettings
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File home
|
||||
private final Persona me
|
||||
private final ChatServer chatServer
|
||||
|
||||
private final Map<InfoHash, Downloader> downloaders = new ConcurrentHashMap<>()
|
||||
|
||||
public DownloadManager(EventBus eventBus, TrustService trustService, MeshManager meshManager, MuWireSettings muSettings,
|
||||
I2PConnector connector, File home, Persona me) {
|
||||
I2PConnector connector, File home, Persona me, ChatServer chatServer) {
|
||||
this.eventBus = eventBus
|
||||
this.trustService = trustService
|
||||
this.meshManager = meshManager
|
||||
@@ -52,6 +55,7 @@ public class DownloadManager {
|
||||
this.connector = connector
|
||||
this.home = home
|
||||
this.me = me
|
||||
this.chatServer = chatServer
|
||||
|
||||
this.executor = Executors.newCachedThreadPool({ r ->
|
||||
Thread rv = new Thread(r)
|
||||
@@ -94,9 +98,9 @@ public class DownloadManager {
|
||||
incompletes.mkdirs()
|
||||
|
||||
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
|
||||
def downloader = new Downloader(eventBus, this, me, target, size,
|
||||
def downloader = new Downloader(eventBus, this, chatServer, me, target, size,
|
||||
infoHash, pieceSize, connector, destinations,
|
||||
incompletes, pieces)
|
||||
incompletes, pieces, muSettings.downloadMaxFailures)
|
||||
downloaders.put(infoHash, downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
@@ -108,6 +112,11 @@ public class DownloadManager {
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
public void onDownloadHopelessEvent(DownloadHopelessEvent e) {
|
||||
downloaders.remove(e.downloader.infoHash)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
|
||||
persistDownloaders()
|
||||
}
|
||||
@@ -158,8 +167,8 @@ public class DownloadManager {
|
||||
|
||||
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
|
||||
def downloader = new Downloader(eventBus, this, chatServer, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces, muSettings.downloadMaxFailures)
|
||||
if (json.paused != null)
|
||||
downloader.paused = json.paused
|
||||
|
||||
|
@@ -37,13 +37,15 @@ class DownloadSession {
|
||||
private final long fileLength
|
||||
private final Set<Integer> available
|
||||
private final MessageDigest digest
|
||||
private final boolean browse, feed, chat
|
||||
|
||||
private final AtomicLong dataSinceLastRead
|
||||
|
||||
private MappedByteBuffer mapped
|
||||
|
||||
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead) {
|
||||
int pieceSize, long fileLength, Set<Integer> available, AtomicLong dataSinceLastRead,
|
||||
boolean browse, boolean feed, boolean chat) {
|
||||
this.eventBus = eventBus
|
||||
this.meB64 = meB64
|
||||
this.pieces = pieces
|
||||
@@ -54,6 +56,9 @@ class DownloadSession {
|
||||
this.fileLength = fileLength
|
||||
this.available = available
|
||||
this.dataSinceLastRead = dataSinceLastRead
|
||||
this.browse = browse
|
||||
this.feed = feed
|
||||
this.chat = chat
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256")
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
@@ -94,6 +99,12 @@ class DownloadSession {
|
||||
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("X-Persona: $meB64\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
if (browse)
|
||||
os.write("Browse: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
if (feed)
|
||||
os.write("Feed: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
if (chat)
|
||||
os.write("Chat: true\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
String xHave = DataUtil.encodeXHave(pieces.getDownloaded(), pieces.nPieces)
|
||||
os.write("X-Have: $xHave\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
@@ -204,6 +215,8 @@ class DownloadSession {
|
||||
pieces.markPartial(piece, 0)
|
||||
throw new BadHashException("bad hash on piece $piece")
|
||||
}
|
||||
|
||||
eventBus.publish(new SourceVerifiedEvent(infoHash : infoHash, source : endpoint.destination))
|
||||
} finally {
|
||||
try { channel?.close() } catch (IOException ignore) {}
|
||||
DataUtil.tryUnmap(mapped)
|
||||
|
@@ -2,6 +2,8 @@ package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatManager
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
import java.nio.file.AtomicMoveNotSupportedException
|
||||
@@ -29,7 +31,7 @@ import net.i2p.util.ConcurrentHashSet
|
||||
@Log
|
||||
public class Downloader {
|
||||
|
||||
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
|
||||
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, HOPELESS, CANCELLED, PAUSED, FINISHED }
|
||||
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||
|
||||
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||
@@ -41,6 +43,7 @@ public class Downloader {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final DownloadManager downloadManager
|
||||
private final ChatServer chatServer
|
||||
private final Persona me
|
||||
private final File file
|
||||
private final Pieces pieces
|
||||
@@ -56,10 +59,14 @@ public class Downloader {
|
||||
final int pieceSizePow2
|
||||
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
|
||||
/** LOCKING: itself */
|
||||
private final Map<Destination, Integer> failingDestinations = new HashMap<>()
|
||||
private final int maxFailures
|
||||
|
||||
|
||||
private volatile boolean cancelled, paused
|
||||
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||
private final AtomicBoolean hopelessEventFired = new AtomicBoolean()
|
||||
private boolean piecesFileClosed
|
||||
|
||||
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
|
||||
@@ -68,13 +75,14 @@ public class Downloader {
|
||||
private int speedPos = 0
|
||||
private int speedAvg = 0
|
||||
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||
public Downloader(EventBus eventBus, DownloadManager downloadManager, ChatServer chatServer,
|
||||
Persona me, File file, long length, InfoHash infoHash,
|
||||
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
|
||||
File incompletes, Pieces pieces) {
|
||||
File incompletes, Pieces pieces, int maxFailures) {
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.downloadManager = downloadManager
|
||||
this.chatServer = chatServer
|
||||
this.file = file
|
||||
this.infoHash = infoHash
|
||||
this.length = length
|
||||
@@ -87,6 +95,7 @@ public class Downloader {
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
this.pieces = pieces
|
||||
this.nPieces = pieces.nPieces
|
||||
this.maxFailures = maxFailures
|
||||
}
|
||||
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
@@ -116,7 +125,7 @@ public class Downloader {
|
||||
void download() {
|
||||
readPieces()
|
||||
destinations.each {
|
||||
if (it != me.destination) {
|
||||
if (it != me.destination && !isHopeless(it)) {
|
||||
def worker = new DownloadWorker(it)
|
||||
activeWorkers.put(it, worker)
|
||||
executorService.submit(worker)
|
||||
@@ -206,6 +215,8 @@ public class Downloader {
|
||||
if (allFinished) {
|
||||
if (pieces.isComplete())
|
||||
return DownloadState.FINISHED
|
||||
if (!hasLiveSources())
|
||||
return DownloadState.HOPELESS
|
||||
return DownloadState.FAILED
|
||||
}
|
||||
|
||||
@@ -269,11 +280,22 @@ public class Downloader {
|
||||
public int getTotalWorkers() {
|
||||
return activeWorkers.size();
|
||||
}
|
||||
|
||||
public int countHopelessSources() {
|
||||
synchronized(failingDestinations) {
|
||||
return destinations.count { isHopeless(it)}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasLiveSources() {
|
||||
destinations.size() > countHopelessSources()
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused = false
|
||||
readPieces()
|
||||
destinations.each { destination ->
|
||||
destinations.stream().filter({!isHopeless(it)}).forEach { destination ->
|
||||
log.fine("resuming source ${destination.toBase32()}")
|
||||
def worker = activeWorkers.get(destination)
|
||||
if (worker != null) {
|
||||
if (worker.currentState == WorkerState.FINISHED) {
|
||||
@@ -290,8 +312,9 @@ public class Downloader {
|
||||
}
|
||||
|
||||
void addSource(Destination d) {
|
||||
if (activeWorkers.containsKey(d))
|
||||
if (activeWorkers.containsKey(d) || isHopeless(d))
|
||||
return
|
||||
destinations.add(d)
|
||||
DownloadWorker newWorker = new DownloadWorker(d)
|
||||
activeWorkers.put(d, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
@@ -347,6 +370,28 @@ public class Downloader {
|
||||
try {os?.close() } catch (IOException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHopeless(Destination d) {
|
||||
if (maxFailures < 0)
|
||||
return false
|
||||
synchronized(failingDestinations) {
|
||||
return !successfulDestinations.contains(d) &&
|
||||
failingDestinations.containsKey(d) &&
|
||||
failingDestinations[d] >= maxFailures
|
||||
}
|
||||
}
|
||||
|
||||
private void markFailed(Destination d) {
|
||||
log.fine("marking failed ${d.toBase32()}")
|
||||
synchronized(failingDestinations) {
|
||||
Integer count = failingDestinations.get(d)
|
||||
if (count == null) {
|
||||
failingDestinations.put(d, 1)
|
||||
} else {
|
||||
failingDestinations.put(d, count + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadWorker implements Runnable {
|
||||
private final Destination destination
|
||||
@@ -373,10 +418,16 @@ public class Downloader {
|
||||
setInfoHash(received)
|
||||
}
|
||||
currentState = WorkerState.DOWNLOADING
|
||||
|
||||
boolean browse = downloadManager.muSettings.browseFiles
|
||||
boolean feed = downloadManager.muSettings.fileFeed && downloadManager.muSettings.advertiseFeed
|
||||
boolean chat = chatServer.isRunning() && downloadManager.muSettings.advertiseChat
|
||||
|
||||
boolean requestPerformed
|
||||
while(!pieces.isComplete()) {
|
||||
currentSession = new DownloadSession(eventBus, me.toBase64(), pieces, getInfoHash(),
|
||||
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead)
|
||||
endpoint, incompleteFile, pieceSize, length, available, dataSinceLastRead,
|
||||
browse, feed, chat)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@@ -385,6 +436,9 @@ public class Downloader {
|
||||
}
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
|
||||
markFailed(destination)
|
||||
if (!hasLiveSources() && hopelessEventFired.compareAndSet(false, true))
|
||||
eventBus.publish(new DownloadHopelessEvent(downloader : Downloader.this))
|
||||
} finally {
|
||||
writePieces()
|
||||
currentState = WorkerState.FINISHED
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class SourceVerifiedEvent extends Event {
|
||||
InfoHash infoHash
|
||||
Destination source
|
||||
}
|
@@ -96,8 +96,12 @@ class WatchedDirectoryManager {
|
||||
forEach {
|
||||
def parsed = slurper.parse(it.toFile())
|
||||
WatchedDirectory wd = WatchedDirectory.fromJson(parsed)
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
if (wd.directory.exists() && wd.directory.isDirectory()) // check if directory disappeared
|
||||
watchedDirs.put(wd.directory, wd)
|
||||
else
|
||||
it.toFile().delete()
|
||||
}
|
||||
|
||||
watchedDirs.values().stream().filter({it.autoWatch}).forEach {
|
||||
eventBus.publish(new DirectoryWatchedEvent(directory : it.directory))
|
||||
eventBus.publish(new FileSharedEvent(file : it.directory))
|
||||
|
@@ -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
|
||||
|
@@ -7,17 +7,19 @@ class Host {
|
||||
private static final int MAX_FAILURES = 3
|
||||
|
||||
final Destination destination
|
||||
private final int clearInterval, hopelessInterval, rejectionInterval
|
||||
private final int clearInterval, hopelessInterval, rejectionInterval, purgeInterval
|
||||
int failures,successes
|
||||
long lastAttempt
|
||||
long lastSuccessfulAttempt
|
||||
long lastRejection
|
||||
|
||||
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
|
||||
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval,
|
||||
int purgeInterval) {
|
||||
this.destination = destination
|
||||
this.clearInterval = clearInterval
|
||||
this.hopelessInterval = hopelessInterval
|
||||
this.rejectionInterval = rejectionInterval
|
||||
this.purgeInterval = purgeInterval
|
||||
}
|
||||
|
||||
private void connectSuccessful() {
|
||||
@@ -54,17 +56,22 @@ class Host {
|
||||
failures = 0
|
||||
}
|
||||
|
||||
synchronized boolean canTryAgain() {
|
||||
synchronized boolean canTryAgain(final long now) {
|
||||
lastSuccessfulAttempt > 0 &&
|
||||
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
|
||||
now - lastAttempt > (clearInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean isHopeless() {
|
||||
synchronized boolean isHopeless(final long now) {
|
||||
isFailed() &&
|
||||
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
|
||||
now - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean isRecentlyRejected() {
|
||||
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
|
||||
synchronized boolean isRecentlyRejected(final long now) {
|
||||
now - lastRejection < (rejectionInterval * 60 * 1000)
|
||||
}
|
||||
|
||||
synchronized boolean shouldBeForgotten(final long now) {
|
||||
isHopeless(now) &&
|
||||
now - lastAttempt > (purgeInterval * 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,8 @@ class HostCache extends Service {
|
||||
hosts.get(e.destination).clearFailures()
|
||||
return
|
||||
}
|
||||
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval,
|
||||
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
|
||||
if (allowHost(host)) {
|
||||
hosts.put(e.destination, host)
|
||||
}
|
||||
@@ -64,7 +65,8 @@ class HostCache extends Service {
|
||||
Destination dest = e.endpoint.destination
|
||||
Host host = hosts.get(dest)
|
||||
if (host == null) {
|
||||
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval,
|
||||
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
|
||||
hosts.put(dest, host)
|
||||
}
|
||||
|
||||
@@ -84,9 +86,10 @@ class HostCache extends Service {
|
||||
List<Destination> getHosts(int n) {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
rv.retainAll {allowHost(hosts[it])}
|
||||
final long now = System.currentTimeMillis()
|
||||
rv.removeAll {
|
||||
def h = hosts[it];
|
||||
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
|
||||
(h.isFailed() && !h.canTryAgain(now)) || h.isRecentlyRejected(now) || h.isHopeless(now)
|
||||
}
|
||||
if (rv.size() <= n)
|
||||
return rv
|
||||
@@ -116,8 +119,9 @@ class HostCache extends Service {
|
||||
|
||||
int countHopelessHosts() {
|
||||
List<Destination> rv = new ArrayList<>(hosts.keySet())
|
||||
final long now = System.currentTimeMillis()
|
||||
rv.retainAll {
|
||||
hosts[it].isHopeless()
|
||||
hosts[it].isHopeless(now)
|
||||
}
|
||||
rv.size()
|
||||
}
|
||||
@@ -128,7 +132,8 @@ class HostCache extends Service {
|
||||
storage.eachLine {
|
||||
def entry = slurper.parseText(it)
|
||||
Destination dest = new Destination(entry.destination)
|
||||
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
|
||||
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval,
|
||||
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
|
||||
host.failures = Integer.valueOf(String.valueOf(entry.failures))
|
||||
host.successes = Integer.valueOf(String.valueOf(entry.successes))
|
||||
if (entry.lastAttempt != null)
|
||||
@@ -161,10 +166,12 @@ class HostCache extends Service {
|
||||
}
|
||||
|
||||
private void save() {
|
||||
final long now = System.currentTimeMillis()
|
||||
hosts.keySet().removeAll { hosts[it].shouldBeForgotten(now) }
|
||||
storage.delete()
|
||||
storage.withPrintWriter { writer ->
|
||||
hosts.each { dest, host ->
|
||||
if (allowHost(host) && !host.isHopeless()) {
|
||||
if (allowHost(host) && !host.isHopeless(now)) {
|
||||
def map = [:]
|
||||
map.destination = dest.toBase64()
|
||||
map.failures = host.failures
|
||||
|
@@ -1,15 +1,29 @@
|
||||
package com.muwire.core.mesh
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
/**
|
||||
* Representation of a download mesh.
|
||||
*
|
||||
* Two data structures - collection of known sources and collection of sources
|
||||
* we have successfully transferred data with.
|
||||
*
|
||||
* @author zab
|
||||
*/
|
||||
class Mesh {
|
||||
private final InfoHash infoHash
|
||||
private final Set<Persona> sources = new ConcurrentHashSet<>()
|
||||
private final Map<Destination,Persona> sources = new HashMap<>()
|
||||
private final Set<Destination> verified = new HashSet<>()
|
||||
final Pieces pieces
|
||||
|
||||
Mesh(InfoHash infoHash, Pieces pieces) {
|
||||
@@ -17,12 +31,38 @@ class Mesh {
|
||||
this.pieces = pieces
|
||||
}
|
||||
|
||||
Set<Persona> getRandom(int n, Persona exclude) {
|
||||
List<Persona> tmp = new ArrayList<>(sources)
|
||||
tmp.remove(exclude)
|
||||
synchronized Set<Persona> getRandom(int n, Persona exclude) {
|
||||
List<Destination> tmp = new ArrayList<>(verified)
|
||||
if (exclude != null)
|
||||
tmp.remove(exclude.destination)
|
||||
tmp.retainAll(sources.keySet()) // verified may contain nodes not in sources
|
||||
Collections.shuffle(tmp)
|
||||
if (tmp.size() < n)
|
||||
return tmp
|
||||
tmp[0..n-1]
|
||||
if (tmp.size() > n)
|
||||
tmp = tmp[0..n-1]
|
||||
tmp.collect(new HashSet<>(), { sources[it] })
|
||||
}
|
||||
|
||||
synchronized void add(Persona persona) {
|
||||
sources.put(persona.destination, persona)
|
||||
}
|
||||
|
||||
synchronized void verify(Destination d) {
|
||||
verified.add(d)
|
||||
}
|
||||
|
||||
synchronized def toJson() {
|
||||
def json = [:]
|
||||
json.timestamp = System.currentTimeMillis()
|
||||
json.infoHash = Base64.encode(infoHash.getRoot())
|
||||
|
||||
Set<Persona> toPersist = new HashSet<>(sources.values())
|
||||
toPersist.retainAll { verified.contains(it.destination) }
|
||||
json.sources = toPersist.collect {it.toBase64()}
|
||||
json.nPieces = pieces.nPieces
|
||||
List<Integer> downloaded = pieces.getDownloaded()
|
||||
if( downloaded.size() > pieces.nPieces)
|
||||
return null
|
||||
json.xHave = DataUtil.encodeXHave(downloaded, pieces.nPieces)
|
||||
json
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.Pieces
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.download.SourceVerifiedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
@@ -56,25 +57,25 @@ class MeshManager {
|
||||
Mesh mesh = meshes.get(e.infoHash)
|
||||
if (mesh == null)
|
||||
return
|
||||
mesh.sources.add(e.source)
|
||||
save()
|
||||
mesh.add(e.source)
|
||||
}
|
||||
|
||||
void onSourceVerifiedEvent(SourceVerifiedEvent e) {
|
||||
Mesh mesh = meshes.get(e.infoHash)
|
||||
if (mesh == null)
|
||||
return
|
||||
mesh.verify(e.source)
|
||||
save()
|
||||
}
|
||||
|
||||
private void save() {
|
||||
File meshFile = new File(home, "mesh.json")
|
||||
synchronized(meshes) {
|
||||
meshFile.withPrintWriter { writer ->
|
||||
meshes.values().each { mesh ->
|
||||
def json = [:]
|
||||
json.timestamp = System.currentTimeMillis()
|
||||
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
|
||||
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
|
||||
json.nPieces = mesh.pieces.nPieces
|
||||
List<Integer> downloaded = mesh.pieces.getDownloaded()
|
||||
if( downloaded.size() > mesh.pieces.nPieces)
|
||||
return
|
||||
json.xHave = DataUtil.encodeXHave(downloaded, mesh.pieces.nPieces)
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
def json = mesh.toJson()
|
||||
if (json != null)
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +100,8 @@ class MeshManager {
|
||||
Mesh mesh = new Mesh(infoHash, pieces)
|
||||
json.sources.each { source ->
|
||||
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
|
||||
mesh.sources.add(persona)
|
||||
mesh.add(persona)
|
||||
mesh.verify(persona.destination) // assume if persisted it was verified
|
||||
}
|
||||
|
||||
if (json.xHave != null) {
|
||||
|
@@ -0,0 +1,30 @@
|
||||
package com.muwire.core.search
|
||||
|
||||
import com.muwire.core.download.SourceVerifiedEvent
|
||||
import com.muwire.core.util.FixedSizeFIFOSet
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
/**
|
||||
* Caches destinations that have recently responded to with results.
|
||||
*/
|
||||
class ResponderCache {
|
||||
|
||||
private final FixedSizeFIFOSet<Destination> cache
|
||||
|
||||
ResponderCache(int capacity) {
|
||||
cache = new FixedSizeFIFOSet<>(capacity)
|
||||
}
|
||||
|
||||
synchronized void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||
cache.add(e.results[0].sender.destination)
|
||||
}
|
||||
|
||||
synchronized void onSourceVerifiedEvent(SourceVerifiedEvent e) {
|
||||
cache.add(e.source)
|
||||
}
|
||||
|
||||
synchronized boolean hasResponded(Destination d) {
|
||||
cache.contains(d)
|
||||
}
|
||||
}
|
@@ -9,9 +9,11 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class ResultsParser {
|
||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||
if (json.type != "Result")
|
||||
@@ -103,6 +105,8 @@ class ResultsParser {
|
||||
int certificates = 0
|
||||
if (json.certificates != null)
|
||||
certificates = json.certificates
|
||||
|
||||
log.fine("Received result from ${p.getHumanReadableName()} name \"$name\" infoHash:\"${json.infohash}\"")
|
||||
|
||||
return new UIResultEvent( sender : p,
|
||||
name : name,
|
||||
|
@@ -88,7 +88,7 @@ class ResultsSender {
|
||||
sources : suggested,
|
||||
comment : comment,
|
||||
certificates : certificates,
|
||||
chat : chatServer.running.get() && settings.advertiseChat,
|
||||
chat : chatServer.isRunning() && settings.advertiseChat,
|
||||
feed : settings.fileFeed && settings.advertiseFeed
|
||||
)
|
||||
uiResultEvents << uiResultEvent
|
||||
@@ -137,7 +137,7 @@ class ResultsSender {
|
||||
os.write("RESULTS $uuid\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Sender: ${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean chat = chatServer.running.get() && settings.advertiseChat
|
||||
boolean chat = chatServer.isRunning() && settings.advertiseChat
|
||||
os.write("Chat: $chat\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean feed = settings.fileFeed && settings.advertiseFeed
|
||||
os.write("Feed: $feed\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
@@ -42,6 +42,8 @@ class TrackerResponder {
|
||||
|
||||
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,
|
||||
@@ -61,6 +63,7 @@ class TrackerResponder {
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
expireTimer.cancel()
|
||||
}
|
||||
|
||||
@@ -200,7 +203,8 @@ class TrackerResponder {
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe("session disconnected")
|
||||
if (!shutdown)
|
||||
log.severe("Tracker Responder session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -49,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) {
|
||||
@@ -69,6 +70,7 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
void stop() {
|
||||
shutdown = true
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
@@ -199,7 +201,8 @@ class UpdateClient {
|
||||
|
||||
@Override
|
||||
public void disconnected(I2PSession session) {
|
||||
log.severe("I2P session disconnected")
|
||||
if (!shutdown)
|
||||
log.severe("I2P session disconnected")
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -3,4 +3,5 @@ package com.muwire.core.upload
|
||||
class ContentRequest extends Request {
|
||||
Range range
|
||||
int have
|
||||
boolean browse, feed, chat
|
||||
}
|
||||
|
@@ -137,4 +137,24 @@ class ContentUploader extends Uploader {
|
||||
request.infoHash == other.request.infoHash &&
|
||||
request.getDownloader() == other.request.getDownloader()
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBrowseEnabled() {
|
||||
request.browse
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFeedEnabled() {
|
||||
request.feed
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChatEnabled() {
|
||||
request.chat
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persona getDownloaderPersona() {
|
||||
request.downloader
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
import net.i2p.data.Base64
|
||||
@@ -75,4 +76,24 @@ class HashListUploader extends Uploader {
|
||||
HashListUploader other = (HashListUploader)o
|
||||
infoHash == other.infoHash && request.downloader == other.request.downloader
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBrowseEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFeedEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChatEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persona getDownloaderPersona() {
|
||||
request.downloader
|
||||
}
|
||||
}
|
||||
|
@@ -55,8 +55,14 @@ class Request {
|
||||
def encoded = headers["X-Have"].trim()
|
||||
have = DataUtil.decodeXHave(encoded).size()
|
||||
}
|
||||
|
||||
boolean browse = headers.containsKey("Browse") && Boolean.parseBoolean(headers['Browse'])
|
||||
boolean feed = headers.containsKey("Feed") && Boolean.parseBoolean(headers['Feed'])
|
||||
boolean chat = headers.containsKey("Chat") && Boolean.parseBoolean(headers['Chat'])
|
||||
|
||||
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||
headers : headers, downloader : downloader, have : have)
|
||||
headers : headers, downloader : downloader, have : have,
|
||||
browse : browse, feed : feed, chat : chat)
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
@@ -11,6 +11,7 @@ import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.download.SourceVerifiedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.PersisterFolderService
|
||||
import com.muwire.core.mesh.Mesh
|
||||
@@ -123,6 +124,7 @@ public class UploadManager {
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
eventBus.publish(new SourceVerifiedEvent(infoHash : request.infoHash, source : request.downloader.destination))
|
||||
} finally {
|
||||
decrementUploads(request.downloader)
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
@@ -259,6 +261,7 @@ public class UploadManager {
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
eventBus.publish(new SourceVerifiedEvent(infoHash : request.infoHash, source : request.downloader.destination))
|
||||
} finally {
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
abstract class Uploader {
|
||||
@@ -31,6 +32,7 @@ abstract class Uploader {
|
||||
}
|
||||
|
||||
abstract String getName();
|
||||
|
||||
|
||||
/**
|
||||
* @return an integer between 0 and 100
|
||||
@@ -38,6 +40,7 @@ abstract class Uploader {
|
||||
abstract int getProgress();
|
||||
|
||||
abstract String getDownloader();
|
||||
abstract Persona getDownloaderPersona();
|
||||
|
||||
abstract int getDonePieces();
|
||||
|
||||
@@ -45,6 +48,10 @@ abstract class Uploader {
|
||||
|
||||
abstract long getTotalSize();
|
||||
|
||||
abstract boolean isBrowseEnabled();
|
||||
abstract boolean isFeedEnabled();
|
||||
abstract boolean isChatEnabled();
|
||||
|
||||
synchronized int speed() {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
|
@@ -6,7 +6,7 @@ public class Feed {
|
||||
|
||||
private final Persona publisher;
|
||||
|
||||
private int updateInterval;
|
||||
private long updateInterval;
|
||||
private long lastUpdated;
|
||||
private volatile long lastUpdateAttempt;
|
||||
private int itemsToKeep;
|
||||
@@ -19,11 +19,11 @@ public class Feed {
|
||||
this.status = FeedFetchStatus.IDLE;
|
||||
}
|
||||
|
||||
public int getUpdateInterval() {
|
||||
public long getUpdateInterval() {
|
||||
return updateInterval;
|
||||
}
|
||||
|
||||
public void setUpdateInterval(int updateInterval) {
|
||||
public void setUpdateInterval(long updateInterval) {
|
||||
this.updateInterval = updateInterval;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,35 @@
|
||||
package com.muwire.core.util;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class FixedSizeFIFOSet<T> {
|
||||
|
||||
private final int capacity;
|
||||
private final Set<T> set = new HashSet<>();
|
||||
private final Deque<T> fifo = new ArrayDeque<>();
|
||||
|
||||
public FixedSizeFIFOSet(final int capacity) {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public boolean contains(T element) {
|
||||
return set.contains(element);
|
||||
}
|
||||
|
||||
public void add(T element) {
|
||||
if (!set.contains(element)) {
|
||||
if (set.size() == capacity) {
|
||||
T toRemove = fifo.removeLast();
|
||||
set.remove(toRemove);
|
||||
}
|
||||
fifo.addFirst(element);
|
||||
set.add(element);
|
||||
} else {
|
||||
fifo.remove(element);
|
||||
fifo.addFirst(element);
|
||||
}
|
||||
}
|
||||
}
|
@@ -46,7 +46,7 @@ class DownloadSessionTest {
|
||||
eventBus = new EventBus()
|
||||
}
|
||||
|
||||
private void initSession(int size, def claimedPieces = []) {
|
||||
private void initSession(int size, def claimedPieces = [], boolean browse = false, boolean feed = false, boolean chat = false) {
|
||||
Random r = new Random()
|
||||
byte [] content = new byte[size]
|
||||
r.nextBytes(content)
|
||||
@@ -78,7 +78,8 @@ class DownloadSessionTest {
|
||||
toUploader = new PipedOutputStream(fromDownloader)
|
||||
endpoint = new Endpoint(null, fromUploader, toUploader, null)
|
||||
|
||||
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong())
|
||||
session = new DownloadSession(eventBus, "",pieces, infoHash, endpoint, target, pieceSize, size, available, new AtomicLong(),
|
||||
browse, feed, chat)
|
||||
downloadThread = new Thread( { perform() } as Runnable)
|
||||
downloadThread.setDaemon(true)
|
||||
downloadThread.start()
|
||||
|
@@ -75,6 +75,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -97,6 +98,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -114,6 +116,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
|
||||
@@ -136,6 +139,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -160,6 +164,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 100 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -182,6 +187,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -211,6 +217,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -246,6 +253,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -266,6 +274,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
|
||||
@@ -301,6 +310,7 @@ class HostCacheTest {
|
||||
settingsMock.ignore.getHostClearInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessInterval { 0 }
|
||||
settingsMock.ignore.getHostRejectInterval { 0 }
|
||||
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
|
||||
|
||||
initMocks()
|
||||
def rv = cache.getHosts(5)
|
||||
|
@@ -0,0 +1,49 @@
|
||||
package com.muwire.core.util
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
class FixedSizeFIFOSetTest {
|
||||
|
||||
@Test
|
||||
public void testFifo() {
|
||||
FixedSizeFIFOSet<String> fifoSet = new FixedSizeFIFOSet(3);
|
||||
fifoSet.add("a")
|
||||
assert fifoSet.contains("a")
|
||||
|
||||
fifoSet.add("b")
|
||||
assert fifoSet.contains("a")
|
||||
assert fifoSet.contains("b")
|
||||
|
||||
fifoSet.add("c")
|
||||
assert fifoSet.contains("a")
|
||||
assert fifoSet.contains("b")
|
||||
assert fifoSet.contains("c")
|
||||
|
||||
fifoSet.add("d")
|
||||
assert !fifoSet.contains("a")
|
||||
assert fifoSet.contains("b")
|
||||
assert fifoSet.contains("c")
|
||||
assert fifoSet.contains("d")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDuplicateElement() {
|
||||
FixedSizeFIFOSet<String> fifoSet = new FixedSizeFIFOSet(3);
|
||||
|
||||
fifoSet.add("a")
|
||||
fifoSet.add("b")
|
||||
fifoSet.add("c")
|
||||
fifoSet.add("a")
|
||||
|
||||
assert fifoSet.contains("a")
|
||||
assert fifoSet.contains("b")
|
||||
assert fifoSet.contains("c")
|
||||
|
||||
fifoSet.add("d")
|
||||
assert fifoSet.contains("a")
|
||||
assert !fifoSet.contains("b")
|
||||
assert fifoSet.contains("c")
|
||||
assert fifoSet.contains("d")
|
||||
}
|
||||
}
|
49
doc/collections.md
Normal file
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,7 +1,7 @@
|
||||
group = com.muwire
|
||||
version = 0.7.0
|
||||
i2pVersion = 0.9.46
|
||||
groovyVersion = 2.4.15
|
||||
version = 0.7.4
|
||||
i2pVersion = 0.9.47
|
||||
groovyVersion = 3.0.4
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
grailsVersion=4.0.0
|
||||
@@ -20,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}"
|
||||
|
||||
@@ -68,6 +101,9 @@ dependencies {
|
||||
runtime group: 'org.slf4j', name: 'jul-to-slf4j', version: "${slf4jVersion}"
|
||||
runtime "javax.annotation:javax.annotation-api:1.3.2"
|
||||
|
||||
// because java 14 doesn't come with it
|
||||
runtime 'mrj:MRJToolkitStubs:1.0'
|
||||
|
||||
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
|
||||
testCompile "org.spockframework:spock-core:${spockVersion}"
|
||||
testCompile('org.awaitility:awaitility-groovy:3.1.0') {
|
||||
|
@@ -15,6 +15,7 @@ import java.util.logging.Level
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.text.StyledDocument
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatCommand
|
||||
@@ -54,14 +55,17 @@ class ChatRoomController {
|
||||
long now = System.currentTimeMillis()
|
||||
|
||||
if (command.action == ChatAction.SAY && command.payload.length() > 0) {
|
||||
String toShow = DataHelper.formatTime(now) + " <" + model.core.me.getHumanReadableName() + "> "+command.payload
|
||||
String header = DataHelper.formatTime(now) + " <" + model.core.me.getHumanReadableName() + ">"
|
||||
StyledDocument sd = view.roomTextArea.getStyledDocument()
|
||||
sd.insertString(sd.getEndPosition().getOffset() - 1, header, sd.getStyle("italic"))
|
||||
sd.insertString(sd.getEndPosition().getOffset() - 1, " ", sd.getStyle("regular"))
|
||||
sd.insertString(sd.getEndPosition().getOffset() - 1, command.payload, sd.getStyle("regular"))
|
||||
sd.insertString(sd.getEndPosition().getOffset() - 1, "\n", sd.getStyle("regular"))
|
||||
|
||||
view.roomTextArea.append(toShow)
|
||||
view.roomTextArea.append('\n')
|
||||
trimLines()
|
||||
}
|
||||
|
||||
if (command.action == ChatAction.JOIN) {
|
||||
if (command.action == ChatAction.JOIN && model.console) {
|
||||
String newRoom = command.payload
|
||||
String groupId = model.host.getHumanReadableName()+"-"+newRoom
|
||||
if (!mvcGroup.parentGroup.childrenGroups.containsKey(groupId)) {
|
||||
@@ -190,9 +194,8 @@ class ChatRoomController {
|
||||
}
|
||||
|
||||
private void processSay(ChatMessageEvent e, String text) {
|
||||
String toDisplay = DataHelper.formatTime(e.timestamp) + " <"+e.sender.getHumanReadableName()+"> " + text + "\n"
|
||||
runInsideUIAsync {
|
||||
view.roomTextArea.append(toDisplay)
|
||||
view.appendSay(text, e.sender, e.timestamp)
|
||||
trimLines()
|
||||
if (!model.console)
|
||||
view.chatNotificator.onMessage(mvcGroup.mvcId)
|
||||
@@ -203,7 +206,7 @@ class ChatRoomController {
|
||||
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " joined the room\n"
|
||||
runInsideUIAsync {
|
||||
model.members.add(p)
|
||||
view.roomTextArea.append(toDisplay)
|
||||
view.appendGray(toDisplay)
|
||||
trimLines()
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
@@ -223,7 +226,7 @@ class ChatRoomController {
|
||||
String toDisplay = DataHelper.formatTime(timestamp) + " " + p.getHumanReadableName() + " left the room\n"
|
||||
runInsideUIAsync {
|
||||
model.members.remove(p)
|
||||
view.roomTextArea.append(toDisplay)
|
||||
view.appendGray(toDisplay)
|
||||
trimLines()
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
@@ -233,7 +236,7 @@ class ChatRoomController {
|
||||
String toDisplay = DataHelper.formatTime(System.currentTimeMillis()) + " " + p.getHumanReadableName() + " disconnected\n"
|
||||
runInsideUIAsync {
|
||||
if (model.members.remove(p)) {
|
||||
view.roomTextArea.append(toDisplay)
|
||||
view.appendGray(toDisplay)
|
||||
trimLines()
|
||||
view.membersTable?.model?.fireTableDataChanged()
|
||||
}
|
||||
@@ -243,11 +246,8 @@ class ChatRoomController {
|
||||
private void trimLines() {
|
||||
if (model.settings.maxChatLines < 0)
|
||||
return
|
||||
while(view.roomTextArea.getLineCount() > model.settings.maxChatLines) {
|
||||
int line0Start = view.roomTextArea.getLineStartOffset(0)
|
||||
int line0End = view.roomTextArea.getLineEndOffset(0)
|
||||
view.roomTextArea.replaceRange(null, line0Start, line0End)
|
||||
}
|
||||
while(view.getLineCount() > model.settings.maxChatLines)
|
||||
view.removeFirstLine()
|
||||
}
|
||||
|
||||
void rejoinRoom() {
|
||||
|
@@ -21,7 +21,7 @@ class FeedConfigurationController {
|
||||
model.feed.setAutoDownload(view.autoDownloadCheckbox.model.isSelected())
|
||||
model.feed.setSequential(view.sequentialCheckbox.model.isSelected())
|
||||
model.feed.setItemsToKeep(Integer.parseInt(view.itemsToKeepField.text))
|
||||
model.feed.setUpdateInterval(Integer.parseInt(view.updateIntervalField.text) * 60000)
|
||||
model.feed.setUpdateInterval(Long.parseLong(view.updateIntervalField.text) * 60000)
|
||||
|
||||
model.core.eventBus.publish(new UIFeedConfigurationEvent(feed : model.feed))
|
||||
|
||||
|
@@ -33,6 +33,7 @@ import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.filefeeds.Feed
|
||||
import com.muwire.core.filefeeds.FeedItem
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
|
||||
import com.muwire.core.filefeeds.UIFeedDeletedEvent
|
||||
import com.muwire.core.filefeeds.UIFeedUpdateEvent
|
||||
import com.muwire.core.filefeeds.UIFilePublishedEvent
|
||||
@@ -61,13 +62,6 @@ class MainFrameController {
|
||||
|
||||
private volatile Core core
|
||||
|
||||
@ControllerAction
|
||||
void clearSearch() {
|
||||
def searchField = builder.getVariable("search-field")
|
||||
searchField.setSelectedItem(null)
|
||||
searchField.requestFocus()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void search(ActionEvent evt) {
|
||||
if (evt?.getActionCommand() == null)
|
||||
@@ -151,6 +145,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
|
||||
|
||||
@@ -227,17 +222,13 @@ class MainFrameController {
|
||||
|
||||
@ControllerAction
|
||||
void clear() {
|
||||
def toRemove = []
|
||||
model.downloads.each {
|
||||
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
|
||||
toRemove << it
|
||||
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
|
||||
toRemove << it
|
||||
}
|
||||
}
|
||||
toRemove.each {
|
||||
model.downloads.remove(it)
|
||||
}
|
||||
model.downloads.removeAll {
|
||||
def state = it.downloader.getCurrentState()
|
||||
state == Downloader.DownloadState.CANCELLED ||
|
||||
state == Downloader.DownloadState.FINISHED ||
|
||||
state == Downloader.DownloadState.HOPELESS
|
||||
}
|
||||
|
||||
model.clearButtonEnabled = false
|
||||
|
||||
}
|
||||
@@ -362,6 +353,47 @@ class MainFrameController {
|
||||
startChat(p)
|
||||
view.showChatWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void browseFromUpload() {
|
||||
Uploader u = view.selectedUploader()
|
||||
if (u == null)
|
||||
return
|
||||
Persona p = u.getDownloaderPersona()
|
||||
|
||||
String groupId = p.getHumanReadableName() + "-browse"
|
||||
def params = [:]
|
||||
params['host'] = p
|
||||
params['core'] = model.core
|
||||
mvcGroup.createMVCGroup("browse",groupId,params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void subscribeFromUpload() {
|
||||
Uploader u = view.selectedUploader()
|
||||
if (u == null)
|
||||
return
|
||||
Persona p = u.getDownloaderPersona()
|
||||
|
||||
Feed feed = new Feed(p)
|
||||
feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload)
|
||||
feed.setSequential(core.muOptions.defaultFeedSequential)
|
||||
feed.setItemsToKeep(core.muOptions.defaultFeedItemsToKeep)
|
||||
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval)
|
||||
|
||||
core.eventBus.publish(new UIFeedConfigurationEvent(feed : feed, newFeed : true))
|
||||
view.showFeedsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void chatFromUpload() {
|
||||
Uploader u = view.selectedUploader()
|
||||
if (u == null)
|
||||
return
|
||||
Persona p = u.getDownloaderPersona()
|
||||
startChat(p)
|
||||
view.showChatWindow.call()
|
||||
}
|
||||
|
||||
void unshareSelectedFile() {
|
||||
def sf = view.selectedSharedFiles()
|
||||
|
@@ -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
|
||||
@@ -58,8 +59,11 @@ class OptionsController {
|
||||
|
||||
text = view.retryField.text
|
||||
model.downloadRetryInterval = text
|
||||
|
||||
settings.downloadRetryInterval = Integer.valueOf(text)
|
||||
|
||||
text = view.downloadMaxFailuresField.text
|
||||
model.downloadMaxFailures = text
|
||||
settings.downloadMaxFailures = Integer.valueOf(text)
|
||||
|
||||
text = view.updateField.text
|
||||
model.updateCheckInterval = text
|
||||
@@ -147,7 +151,7 @@ class OptionsController {
|
||||
|
||||
String defaultFeedUpdateInterval = view.defaultFeedUpdateIntervalField.text
|
||||
model.defaultFeedUpdateInterval = defaultFeedUpdateInterval
|
||||
settings.defaultFeedUpdateInterval = Integer.parseInt(defaultFeedUpdateInterval)
|
||||
settings.defaultFeedUpdateInterval = Integer.parseInt(defaultFeedUpdateInterval) * 60000L
|
||||
|
||||
// trust saving
|
||||
|
||||
@@ -200,6 +204,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
|
||||
|
||||
|
@@ -119,7 +119,7 @@ class SearchTabController {
|
||||
feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload)
|
||||
feed.setSequential(core.muOptions.defaultFeedSequential)
|
||||
feed.setItemsToKeep(core.muOptions.defaultFeedItemsToKeep)
|
||||
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval * 60 * 1000)
|
||||
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval)
|
||||
|
||||
core.eventBus.publish(new UIFeedConfigurationEvent(feed : feed, newFeed: true))
|
||||
mvcGroup.parentGroup.view.showFeedsWindow.call()
|
||||
|
@@ -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,16 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
Properties props = new Properties()
|
||||
uiSettings = new UISettings(props)
|
||||
log.info "will try default lnfs"
|
||||
if (isMacOSX()) {
|
||||
if (SystemVersion.isJava9()) {
|
||||
uiSettings.lnf = "metal"
|
||||
lookAndFeel("metal")
|
||||
} else {
|
||||
uiSettings.lnf = "nimbus"
|
||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||
}
|
||||
} else {
|
||||
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||
if (chosen == null)
|
||||
chosen = lookAndFeel('metal')
|
||||
uiSettings.lnf = chosen.getID()
|
||||
log.info("ended up applying $chosen.name")
|
||||
}
|
||||
|
||||
LookAndFeel chosen = lookAndFeel('system', 'gtk', '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)
|
||||
|
@@ -12,6 +12,7 @@ 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
|
||||
@@ -46,21 +47,32 @@ 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")
|
||||
|
||||
if (System.getProperties().containsKey("disableUpdates"))
|
||||
props.disableUpdates = Boolean.valueOf(System.getProperty("disableUpdates"))
|
||||
} else {
|
||||
log.info("creating new properties")
|
||||
props = new MuWireSettings()
|
||||
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
|
||||
@@ -79,6 +91,7 @@ class Ready extends AbstractLifecycleHandler {
|
||||
|
||||
props.embeddedRouter = embeddedRouterAvailable
|
||||
props.updateType = System.getProperty("updateType","jar")
|
||||
props.disableUpdates = Boolean.parseBoolean(System.getProperty("disableUpdates", "false"))
|
||||
|
||||
|
||||
propsFile.withPrintWriter("UTF-8", {
|
||||
@@ -107,32 +120,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 (!DataUtil.isValidName(nickname)) {
|
||||
JOptionPane.showMessageDialog(null,
|
||||
"Nickname cannot contain any of ${Constants.INVALID_NICKNAME_CHARS} and must be no longer than ${Constants.MAX_NICKNAME_LENGTH} characters. Choose another.",
|
||||
"Select another nickname", JOptionPane.WARNING_MESSAGE)
|
||||
continue
|
||||
}
|
||||
nickname = nickname.trim()
|
||||
break
|
||||
}
|
||||
nickname
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,7 @@ class AdvancedSharingModel {
|
||||
Core core
|
||||
|
||||
@Observable boolean syncActionEnabled
|
||||
@Observable boolean configureActionEnabled
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
watchedDirectories.addAll(core.watchedDirectoryManager.watchedDirs.values())
|
||||
|
@@ -177,22 +177,25 @@ class MainFrameModel {
|
||||
if (!mvcGroup.alive)
|
||||
return
|
||||
|
||||
// remove cancelled or finished downloads
|
||||
// remove cancelled or finished or hopeless downloads
|
||||
if (!clearButtonEnabled || uiSettings.clearCancelledDownloads || uiSettings.clearFinishedDownloads) {
|
||||
def toRemove = []
|
||||
downloads.each {
|
||||
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
|
||||
def state = it.downloader.getCurrentState()
|
||||
if (state == Downloader.DownloadState.CANCELLED) {
|
||||
if (uiSettings.clearCancelledDownloads) {
|
||||
toRemove << it
|
||||
} else {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
|
||||
} else if (state == Downloader.DownloadState.FINISHED) {
|
||||
if (uiSettings.clearFinishedDownloads) {
|
||||
toRemove << it
|
||||
} else {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
} else if (state == Downloader.DownloadState.HOPELESS) {
|
||||
clearButtonEnabled = true
|
||||
}
|
||||
}
|
||||
toRemove.each {
|
||||
@@ -245,7 +248,7 @@ class MainFrameModel {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: true, add: true))
|
||||
}
|
||||
|
||||
chatServerRunning = core.chatServer.running.get()
|
||||
chatServerRunning = core.chatServer.isRunning()
|
||||
|
||||
timer.schedule({
|
||||
if (core.shutdown.get())
|
||||
@@ -501,6 +504,9 @@ class MainFrameModel {
|
||||
}
|
||||
|
||||
void onQueryEvent(QueryEvent e) {
|
||||
if (!uiSettings.showMonitor)
|
||||
return
|
||||
|
||||
if (e.replyTo == core.me.destination)
|
||||
return
|
||||
|
||||
|
@@ -7,9 +7,12 @@ import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import java.awt.Font
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class OptionsModel {
|
||||
@Observable String downloadRetryInterval
|
||||
@Observable String downloadMaxFailures
|
||||
@Observable String updateCheckInterval
|
||||
@Observable boolean autoDownloadUpdate
|
||||
@Observable boolean shareDownloadedFiles
|
||||
@@ -36,6 +39,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
|
||||
@@ -70,10 +75,13 @@ class OptionsModel {
|
||||
@Observable boolean advertiseChat
|
||||
@Observable int maxChatLines
|
||||
@Observable String chatWelcomeFile
|
||||
|
||||
boolean disableUpdates
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
downloadRetryInterval = settings.downloadRetryInterval
|
||||
downloadMaxFailures = settings.downloadMaxFailures
|
||||
updateCheckInterval = settings.updateCheckInterval
|
||||
autoDownloadUpdate = settings.autoDownloadUpdate
|
||||
shareDownloadedFiles = settings.shareDownloadedFiles
|
||||
@@ -99,6 +107,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
|
||||
@@ -119,7 +129,7 @@ class OptionsModel {
|
||||
defaultFeedAutoDownload = settings.defaultFeedAutoDownload
|
||||
defaultFeedItemsToKeep = String.valueOf(settings.defaultFeedItemsToKeep)
|
||||
defaultFeedSequential = settings.defaultFeedSequential
|
||||
defaultFeedUpdateInterval = String.valueOf(settings.defaultFeedUpdateInterval)
|
||||
defaultFeedUpdateInterval = String.valueOf(Math.max(1L, (long)(settings.defaultFeedUpdateInterval / 60000L)))
|
||||
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
searchExtraHop = settings.searchExtraHop
|
||||
@@ -131,5 +141,7 @@ class OptionsModel {
|
||||
advertiseChat = settings.advertiseChat
|
||||
maxChatLines = uiSettings.maxChatLines
|
||||
chatWelcomeFile = settings.chatWelcomeFile?.getAbsolutePath()
|
||||
|
||||
disableUpdates = settings.disableUpdates
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ import griffon.metadata.ArtifactProviderFor
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class WizardModel {
|
||||
Component parent
|
||||
WizardDefaults defaults
|
||||
boolean embeddedRouterAvailable
|
||||
MuWireSettings muSettings
|
||||
Properties i2pProps
|
||||
@@ -26,12 +27,12 @@ class WizardModel {
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
|
||||
steps << new NicknameStep()
|
||||
steps << new DirectoriesStep()
|
||||
steps << new DirectoriesStep(defaults)
|
||||
if (embeddedRouterAvailable)
|
||||
steps << new EmbeddedRouterStep()
|
||||
steps << new EmbeddedRouterStep(defaults)
|
||||
else
|
||||
steps << new ExternalRouterStep()
|
||||
steps << new TunnelStep()
|
||||
steps << new ExternalRouterStep(defaults)
|
||||
steps << new TunnelStep(defaults)
|
||||
steps << new LastStep(embeddedRouterAvailable)
|
||||
|
||||
currentStep = 0
|
||||
|
@@ -64,7 +64,7 @@ class AdvancedSharingView {
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Configure", configureAction)
|
||||
button(text : "Configure", enabled : bind{model.configureActionEnabled}, configureAction)
|
||||
button(text : "Sync", enabled : bind{model.syncActionEnabled}, syncAction)
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ class AdvancedSharingView {
|
||||
selectionModel.addListSelectionListener({
|
||||
def directory = selectedWatchedDirectory()
|
||||
model.syncActionEnabled = !(directory == null || directory.autoWatch)
|
||||
model.configureActionEnabled = directory != null
|
||||
})
|
||||
|
||||
watchedDirsTable.addMouseListener(new MouseAdapter() {
|
||||
|
@@ -3,18 +3,26 @@ 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.JMenuItem
|
||||
import javax.swing.JPopupMenu
|
||||
import javax.swing.JSplitPane
|
||||
import javax.swing.JTextPane
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.text.Element
|
||||
import javax.swing.text.Style
|
||||
import javax.swing.text.StyleConstants
|
||||
import javax.swing.text.StyleContext
|
||||
import javax.swing.text.StyledDocument
|
||||
import javax.swing.SpringLayout.Constraints
|
||||
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.chat.ChatConnectionAttemptStatus
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
@@ -34,7 +42,7 @@ class ChatRoomView {
|
||||
def pane
|
||||
def parent
|
||||
def sayField
|
||||
def roomTextArea
|
||||
JTextPane roomTextArea
|
||||
def textScrollPane
|
||||
def membersTable
|
||||
def lastMembersTableSortEvent
|
||||
@@ -48,7 +56,7 @@ class ChatRoomView {
|
||||
panel(constraints : BorderLayout.CENTER) {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
textScrollPane = scrollPane {
|
||||
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
||||
roomTextArea = textPane(editable : false)
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
@@ -78,7 +86,7 @@ class ChatRoomView {
|
||||
panel {
|
||||
gridLayout(rows : 1, cols : 1)
|
||||
textScrollPane = scrollPane {
|
||||
roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true)
|
||||
roomTextArea = textPane(editable : false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +143,15 @@ class ChatRoomView {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// styles
|
||||
StyledDocument document = roomTextArea.getStyledDocument()
|
||||
Style regular = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE)
|
||||
Style italic = document.addStyle("italic", regular)
|
||||
StyleConstants.setItalic(italic, true)
|
||||
Style gray = document.addStyle("gray", regular)
|
||||
StyleConstants.setForeground(gray, Color.GRAY)
|
||||
|
||||
}
|
||||
|
||||
private void showPopupMenu(MouseEvent e) {
|
||||
@@ -179,4 +196,28 @@ class ChatRoomView {
|
||||
chatNotificator.roomClosed(mvcGroup.mvcId)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
|
||||
void appendGray(String gray) {
|
||||
StyledDocument doc = roomTextArea.getStyledDocument()
|
||||
doc.insertString(doc.getEndPosition().getOffset() - 1, gray, doc.getStyle("gray"))
|
||||
}
|
||||
|
||||
void appendSay(String text, Persona sender, long timestamp) {
|
||||
StyledDocument doc = roomTextArea.getStyledDocument()
|
||||
String header = DataHelper.formatTime(timestamp) + " <" + sender.getHumanReadableName() + "> "
|
||||
doc.insertString(doc.getEndPosition().getOffset() - 1, header, doc.getStyle("italic"))
|
||||
doc.insertString(doc.getEndPosition().getOffset() - 1, text, doc.getStyle("regular"))
|
||||
doc.insertString(doc.getEndPosition().getOffset() - 1, "\n", doc.getStyle("regular"))
|
||||
}
|
||||
|
||||
int getLineCount() {
|
||||
StyledDocument doc = roomTextArea.getStyledDocument()
|
||||
doc.getDefaultRootElement().getElementCount() - 1
|
||||
}
|
||||
|
||||
void removeFirstLine() {
|
||||
StyledDocument doc = roomTextArea.getStyledDocument()
|
||||
Element element = doc.getParagraphElement(0)
|
||||
doc.remove(0, element.getEndOffset())
|
||||
}
|
||||
}
|
@@ -44,6 +44,8 @@ import com.muwire.core.filefeeds.FeedFetchStatus
|
||||
import com.muwire.core.filefeeds.FeedItem
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
import com.muwire.core.upload.Uploader
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.FlowLayout
|
||||
@@ -91,7 +93,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'] + " " +
|
||||
@@ -168,12 +170,12 @@ 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) {
|
||||
borderLayout()
|
||||
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
|
||||
label(" Enter search ", constraints: BorderLayout.WEST) // TODO: fix this
|
||||
|
||||
def searchFieldModel = new SearchFieldModel(settings, new File(application.context.get("muwire-home")))
|
||||
JComboBox myComboBox = new SearchField(searchFieldModel)
|
||||
@@ -183,7 +185,6 @@ class MainFrameView {
|
||||
}
|
||||
panel( constraints: BorderLayout.EAST) {
|
||||
button(text: "Search", searchAction)
|
||||
button(text : "", icon : imageIcon("/close_tab.png"), clearSearchAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,10 +261,14 @@ class MainFrameView {
|
||||
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20]))
|
||||
label(text : "Piece Size", constraints : gbc(gridx: 0, gridy:1))
|
||||
label(text : bind {model.downloader?.pieceSize}, constraints : gbc(gridx:1, gridy:1))
|
||||
label(text : "Sequential", constraints : gbc(gridx: 0, gridy: 2))
|
||||
label(text : bind {model.downloader?.isSequential()}, constraints : gbc(gridx:1, gridy:2, insets : [0,0,0,20]))
|
||||
label(text : "Known Sources:", constraints : gbc(gridx:3, gridy: 0))
|
||||
label(text : bind {model.downloader?.activeWorkers?.size()}, constraints : gbc(gridx:4, gridy:0, insets : [0,0,0,20]))
|
||||
label(text : "Active Sources:", constraints : gbc(gridx:3, gridy:1))
|
||||
label(text : bind {model.downloader?.activeWorkers()}, constraints : gbc(gridx:4, gridy:1, insets : [0,0,0,20]))
|
||||
label(text : "Hopeless Sources:", constraints : gbc(gridx:3, gridy:2))
|
||||
label(text : bind {model.downloader?.countHopelessSources()}, constraints : gbc(gridx:4, gridy:2, insets : [0,0,0,20]))
|
||||
label(text : "Total Pieces:", constraints : gbc(gridx:5, gridy: 0))
|
||||
label(text : bind {model.downloader?.nPieces}, constraints : gbc(gridx:6, gridy:0, insets : [0,0,0,20]))
|
||||
label(text : "Done Pieces:", constraints: gbc(gridx:5, gridy: 1))
|
||||
@@ -329,16 +334,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,9 +570,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:")
|
||||
@@ -782,18 +788,14 @@ class MainFrameView {
|
||||
|
||||
selectionModel = uploadsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
JPopupMenu uploadsTableMenu = new JPopupMenu()
|
||||
JMenuItem showInLibrary = new JMenuItem("Show in library")
|
||||
showInLibrary.addActionListener({mvcGroup.controller.showInLibrary()})
|
||||
uploadsTableMenu.add(showInLibrary)
|
||||
uploadsTable.addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showPopupMenu(uploadsTableMenu, e)
|
||||
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
|
||||
showUploadsMenu(e)
|
||||
}
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.isPopupTrigger())
|
||||
showPopupMenu(uploadsTableMenu, e)
|
||||
if (e.isPopupTrigger() || e.button == MouseEvent.BUTTON3)
|
||||
showUploadsMenu(e)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1205,7 +1207,6 @@ class MainFrameView {
|
||||
List<FeedItem> items = selectedFeedItems()
|
||||
if (items == null || items.isEmpty())
|
||||
return
|
||||
// TODO: finish
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
if (model.downloadFeedItemButtonEnabled) {
|
||||
JMenuItem download = new JMenuItem("Download")
|
||||
@@ -1255,6 +1256,37 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
|
||||
void showUploadsMenu(MouseEvent e) {
|
||||
Uploader uploader = selectedUploader()
|
||||
if (uploader == null)
|
||||
return
|
||||
|
||||
JPopupMenu uploadsTableMenu = new JPopupMenu()
|
||||
JMenuItem showInLibrary = new JMenuItem("Show in library")
|
||||
showInLibrary.addActionListener({mvcGroup.controller.showInLibrary()})
|
||||
uploadsTableMenu.add(showInLibrary)
|
||||
|
||||
if (uploader.isBrowseEnabled()) {
|
||||
JMenuItem browseItem = new JMenuItem("Browse Host")
|
||||
browseItem.addActionListener({mvcGroup.controller.browseFromUpload()})
|
||||
uploadsTableMenu.add(browseItem)
|
||||
}
|
||||
|
||||
if (uploader.isFeedEnabled() && mvcGroup.controller.core.feedManager.getFeed(uploader.getDownloaderPersona()) == null) {
|
||||
JMenuItem feedItem = new JMenuItem("Subscribe")
|
||||
feedItem.addActionListener({mvcGroup.controller.subscribeFromUpload()})
|
||||
uploadsTableMenu.add(feedItem)
|
||||
}
|
||||
|
||||
if (uploader.isChatEnabled() && !mvcGroup.controller.core.chatManager.isConnected(uploader.getDownloaderPersona())) {
|
||||
JMenuItem chatItem = new JMenuItem("Chat")
|
||||
chatItem.addActionListener({mvcGroup.controller.chatFromUpload()})
|
||||
uploadsTableMenu.add(chatItem)
|
||||
}
|
||||
|
||||
showPopupMenu(uploadsTableMenu, e)
|
||||
}
|
||||
|
||||
void showRestoreOrEmpty() {
|
||||
def searchWindow = builder.getVariable("search window")
|
||||
String id
|
||||
@@ -1406,8 +1438,12 @@ class MainFrameView {
|
||||
|
||||
expanded.each { tree.expandPath(it) }
|
||||
tree.setSelectionPaths(selectedPaths)
|
||||
|
||||
builder.getVariable("shared-files-table").model.fireTableDataChanged()
|
||||
|
||||
def table = builder.getVariable("shared-files-table")
|
||||
int [] selectedRows = table.getSelectedRows()
|
||||
table.model.fireTableDataChanged()
|
||||
for (int row : selectedRows)
|
||||
table.selectionModel.addSelectionInterval(row, row)
|
||||
}
|
||||
|
||||
public void refreshFeeds() {
|
||||
@@ -1456,10 +1492,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) {
|
||||
@@ -1469,6 +1505,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 {
|
||||
|
@@ -39,6 +39,7 @@ class OptionsView {
|
||||
def chat
|
||||
|
||||
def retryField
|
||||
def downloadMaxFailuresField
|
||||
def updateField
|
||||
def autoDownloadUpdateCheckbox
|
||||
def shareDownloadedCheckbox
|
||||
@@ -59,6 +60,8 @@ class OptionsView {
|
||||
def monitorCheckbox
|
||||
def fontField
|
||||
def fontSizeField
|
||||
def fontStyleBoldCheckbox
|
||||
def fontStyleItalicCheckbox
|
||||
def clearCancelledDownloadsCheckbox
|
||||
def clearFinishedDownloadsCheckbox
|
||||
def excludeLocalResultCheckbox
|
||||
@@ -121,13 +124,17 @@ class OptionsView {
|
||||
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2,
|
||||
constraints : gbc(gridx: 2, gridy: 0, anchor : GridBagConstraints.LINE_END, weightx: 0))
|
||||
|
||||
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_START))
|
||||
button(text : "Choose", constraints : gbc(gridx : 2, gridy:1), downloadLocationAction)
|
||||
label(text : "Give up on sources after this many failures (-1 means never)", constraints: gbc(gridx: 0, gridy: 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
downloadMaxFailuresField = textField(text : bind { model.downloadMaxFailures }, columns : 2,
|
||||
constraints : gbc(gridx: 2, gridy: 1, anchor : GridBagConstraints.LINE_END, weightx: 0))
|
||||
|
||||
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
|
||||
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction)
|
||||
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
|
||||
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), downloadLocationAction)
|
||||
|
||||
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START))
|
||||
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:3, anchor : GridBagConstraints.LINE_START))
|
||||
button(text : "Choose", constraints : gbc(gridx : 2, gridy:3), incompleteLocationAction)
|
||||
}
|
||||
|
||||
panel (border : titledBorder(title : "Upload Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
@@ -151,16 +158,18 @@ class OptionsView {
|
||||
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:1, weightx : 0))
|
||||
}
|
||||
|
||||
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
if (!model.disableUpdates) {
|
||||
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
|
||||
constraints : gbc(gridx : 0, gridy : 4, fill : GridBagConstraints.HORIZONTAL))) {
|
||||
gridBagLayout()
|
||||
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
|
||||
gridBagLayout()
|
||||
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
|
||||
|
||||
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
|
||||
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
|
||||
constraints : gbc(gridx:1, gridy : 1, anchor : GridBagConstraints.LINE_END))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
i = builder.panel {
|
||||
@@ -220,6 +229,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),
|
||||
|
@@ -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)
|
||||
|
@@ -13,14 +13,12 @@ class DirectoriesStep extends WizardStep {
|
||||
def downloadLocationButton
|
||||
def incompleteLocationButton
|
||||
|
||||
public DirectoriesStep(String constraint) {
|
||||
super("directories")
|
||||
public DirectoriesStep(WizardDefaults defaults) {
|
||||
super("directories", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
File defaultDownloadLocation = new File(System.getProperty("user.home"), "MuWire Downloads")
|
||||
File defaultIncompleteLocation = new File(System.getProperty("user.home"), "MuWire Incompletes")
|
||||
|
||||
builder.panel(constraints : getConstraint()) {
|
||||
gridBagLayout()
|
||||
@@ -30,11 +28,11 @@ class DirectoriesStep extends WizardStep {
|
||||
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 : defaultDownloadLocation.getAbsolutePath(),
|
||||
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 : defaultIncompleteLocation.getAbsolutePath(),
|
||||
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)
|
||||
}
|
||||
|
@@ -15,31 +15,29 @@ class EmbeddedRouterStep extends WizardStep {
|
||||
def inBwField
|
||||
def outBwField
|
||||
|
||||
public EmbeddedRouterStep() {
|
||||
super("router")
|
||||
public EmbeddedRouterStep(WizardDefaults defaults) {
|
||||
super("router", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildUI(FactoryBuilderSupport builder) {
|
||||
Random r = new Random()
|
||||
int port = 9151 + r.nextInt(1 + 30777 - 9151) // this range matches what the i2p router would choose
|
||||
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(port), columns : 4, constraints : gbc(gridx:1, gridy:0, anchor : GridBagConstraints.LINE_END))
|
||||
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(port), columns : 4, constraints : gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_END))
|
||||
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 : "512", columns : 3, constraints : gbc(gridx : 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
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 : "256", columns : 3, constraints : gbc(gridx : 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
|
||||
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))
|
||||
}
|
||||
|
@@ -11,8 +11,8 @@ class ExternalRouterStep extends WizardStep {
|
||||
def addressField
|
||||
def portField
|
||||
|
||||
public ExternalRouterStep() {
|
||||
super("router")
|
||||
public ExternalRouterStep(WizardDefaults defaults) {
|
||||
super("router", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -24,10 +24,10 @@ class ExternalRouterStep extends WizardStep {
|
||||
gridBagLayout()
|
||||
|
||||
label(text : "Host", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
|
||||
addressField = textField(text : "127.0.0.1", constraints : gbc(gridx:1, gridy:0, anchor: GridBagConstraints.LINE_END))
|
||||
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 : "7654", constraints : gbc(gridx:1, gridy:1, anchor: GridBagConstraints.LINE_END))
|
||||
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))
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ class LastStep extends WizardStep {
|
||||
private final boolean embeddedRouterAvailable
|
||||
|
||||
public LastStep(boolean embeddedRouterAvailable) {
|
||||
super("last")
|
||||
super("last", null)
|
||||
this.embeddedRouterAvailable = embeddedRouterAvailable
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ class NicknameStep extends WizardStep {
|
||||
volatile def nickField
|
||||
|
||||
public NicknameStep() {
|
||||
super("nickname")
|
||||
super("nickname", null)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -13,8 +13,8 @@ class TunnelStep extends WizardStep {
|
||||
def tunnelLengthSlider
|
||||
def tunnelQuantitySlider
|
||||
|
||||
public TunnelStep() {
|
||||
super("tunnels")
|
||||
public TunnelStep(WizardDefaults defaults) {
|
||||
super("tunnels", defaults)
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -26,7 +26,7 @@ class TunnelStep extends WizardStep {
|
||||
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 : 3,
|
||||
tunnelLengthSlider = slider(minimum : 1, maximum : 3, value : defaults.tunnelLength,
|
||||
majorTickSpacing : 1, snapToTicks: true, paintTicks: true, labelTable : lengthTable,
|
||||
paintLabels : true)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class TunnelStep extends WizardStep {
|
||||
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 : 4,
|
||||
tunnelQuantitySlider = slider(minimum : 1, maximum : 6, value : defaults.tunnelQuantity,
|
||||
majorTickSpacing : 1, snapToTicks : true, paintTicks: true, labelTable : quantityTable,
|
||||
paintLabels : true)
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
@@ -5,9 +5,11 @@ import com.muwire.core.MuWireSettings
|
||||
abstract class WizardStep {
|
||||
|
||||
final String constraint
|
||||
final WizardDefaults defaults
|
||||
|
||||
protected WizardStep(String constraint) {
|
||||
protected WizardStep(String constraint, WizardDefaults defaults) {
|
||||
this.constraint = constraint
|
||||
this.defaults = defaults
|
||||
}
|
||||
|
||||
|
||||
|
@@ -6,5 +6,9 @@ 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:2.4.15'
|
||||
testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
|
||||
}
|
||||
|
||||
configurations.testImplementation {
|
||||
exclude group:'org.codehaus.groovy', module:'groovy-testng'
|
||||
}
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -42,8 +42,12 @@ class UpdateServer {
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
def props = System.getProperties().clone()
|
||||
props.putAt("inbound.nickname", "MuWire UpdateServer")
|
||||
Properties props = System.getProperties().clone()
|
||||
props["inbound.nickname"] = "MuWire UpdateServer"
|
||||
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()
|
||||
|
||||
|
@@ -349,9 +349,15 @@ div#uploads table thead th:nth-child(3) {
|
||||
width: 120px;
|
||||
}
|
||||
div#uploads table thead th:nth-child(4) {
|
||||
width: 120px;
|
||||
width: 70px;
|
||||
}
|
||||
div#uploads table thead th:nth-child(5) {
|
||||
width: 70px;
|
||||
}
|
||||
div#uploads table thead th:nth-child(6) {
|
||||
width: 120px;
|
||||
}
|
||||
div#uploads table thead th:nth-child(7) {
|
||||
width: 120px;
|
||||
}
|
||||
div#uploads table tbody td:nth-child(1) {
|
||||
@@ -363,11 +369,11 @@ div#uploads table tbody td:nth-child(2) {
|
||||
div#uploads table tbody td:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
div#uploads table tbody td:nth-child(4) {
|
||||
div#uploads table tbody td:nth-child(6) {
|
||||
padding-right: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
div#uploads table tbody td:nth-child(5) {
|
||||
div#uploads table tbody td:nth-child(7) {
|
||||
padding-right: 25px;
|
||||
text-align: right;
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
webui/src/main/images/muwire_logo.png
Normal file
BIN
webui/src/main/images/muwire_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@@ -23,6 +23,7 @@ public class ConfigurationServlet extends HttpServlet {
|
||||
static {
|
||||
INPUT_VALIDATORS.put("trustListInterval", new PositiveIntegerValidator("Trust list update frequency (hours)"));
|
||||
INPUT_VALIDATORS.put("downloadRetryInterval", new PositiveIntegerValidator("Download retry frequency (seconds)"));
|
||||
INPUT_VALIDATORS.put("downloadMaxFailures", new IntegerValidator("Give up on sources after this many failures (-1 means never)"));
|
||||
INPUT_VALIDATORS.put("totalUploadSlots", new IntegerValidator("Total upload slots (-1 means unlimited)"));
|
||||
INPUT_VALIDATORS.put("uploadSlotsPerUser", new IntegerValidator("Upload slots per user (-1 means unlimited)"));
|
||||
INPUT_VALIDATORS.put("downloadLocation", new DirectoryValidator());
|
||||
@@ -92,6 +93,7 @@ public class ConfigurationServlet extends HttpServlet {
|
||||
case "allowTrustLists": core.getMuOptions().setAllowTrustLists(true); break;
|
||||
case "trustListInterval" : core.getMuOptions().setTrustListInterval(Integer.parseInt(value)); break;
|
||||
case "downloadRetryInterval" : core.getMuOptions().setDownloadRetryInterval(Integer.parseInt(value)); break;
|
||||
case "downloadMaxFailures" : core.getMuOptions().setDownloadMaxFailures(Integer.parseInt(value)); break;
|
||||
case "totalUploadSlots" : core.getMuOptions().setTotalUploadSlots(Integer.parseInt(value)); break;
|
||||
case "uploadSlotsPerUser" : core.getMuOptions().setUploadSlotsPerUser(Integer.parseInt(value)); break;
|
||||
case "downloadLocation" : core.getMuOptions().setDownloadLocation(getDirectory(value)); break;
|
||||
@@ -111,7 +113,7 @@ public class ConfigurationServlet extends HttpServlet {
|
||||
case "autoPublishSharedFiles" : core.getMuOptions().setAutoPublishSharedFiles(true); break;
|
||||
case "defaultFeedAutoDownload" : core.getMuOptions().setDefaultFeedAutoDownload(true); break;
|
||||
case "defaultFeedSequential" : core.getMuOptions().setDefaultFeedSequential(true); break;
|
||||
case "defaultFeedUpdateInterval" : core.getMuOptions().setDefaultFeedUpdateInterval(60000 * Integer.parseInt(value)); break;
|
||||
case "defaultFeedUpdateInterval" : core.getMuOptions().setDefaultFeedUpdateInterval(60000 * Long.parseLong(value)); break;
|
||||
case "defaultFeedItemsToKeep" : core.getMuOptions().setDefaultFeedItemsToKeep(Integer.parseInt(value)); break;
|
||||
|
||||
// TODO: ui settings
|
||||
|
@@ -63,7 +63,8 @@ public class DownloadManager {
|
||||
Map.Entry<InfoHash, Downloader> entry = iter.next();
|
||||
Downloader.DownloadState state = entry.getValue().getCurrentState();
|
||||
if (state == Downloader.DownloadState.CANCELLED ||
|
||||
state == Downloader.DownloadState.FINISHED)
|
||||
state == Downloader.DownloadState.FINISHED ||
|
||||
state == Downloader.DownloadState.HOPELESS)
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
@@ -104,8 +104,10 @@ public class DownloadServlet extends HttpServlet {
|
||||
sb.append("<Details>");
|
||||
sb.append("<Path>").append(Util.escapeHTMLinXML(downloader.getFile().getAbsolutePath())).append("</Path>");
|
||||
sb.append("<PieceSize>").append(downloader.getPieceSize()).append("</PieceSize>");
|
||||
sb.append("<Sequential>").append(downloader.isSequential()).append("</Sequential>");
|
||||
sb.append("<KnownSources>").append(downloader.getTotalWorkers()).append("</KnownSources>");
|
||||
sb.append("<ActiveSources>").append(downloader.activeWorkers()).append("</ActiveSources>");
|
||||
sb.append("<HopelessSources>").append(downloader.countHopelessSources()).append("</HopelessSources>");
|
||||
sb.append("<TotalPieces>").append(downloader.getNPieces()).append("</TotalPieces>");
|
||||
sb.append("<DonePieces>").append(downloader.donePieces()).append("</DonePieces>");
|
||||
sb.append("</Details>");
|
||||
@@ -213,7 +215,7 @@ public class DownloadServlet extends HttpServlet {
|
||||
void toXML(StringBuilder sb) {
|
||||
sb.append("<Download>");
|
||||
sb.append("<InfoHash>").append(Base64.encode(infoHash.getRoot())).append("</InfoHash>");
|
||||
sb.append("<Name>").append(name).append("</Name>");
|
||||
sb.append("<Name>").append(Util.escapeHTMLinXML(name)).append("</Name>");
|
||||
sb.append("<State>").append(state.toString()).append("</State>");
|
||||
sb.append("<Speed>").append(DataHelper.formatSize2Decimal(speed, false)).append("B/sec").append("</Speed>");
|
||||
String ETAString;
|
||||
|
@@ -104,7 +104,7 @@ public class FeedManager {
|
||||
}
|
||||
|
||||
void configure(Persona publisher, boolean autoDownload, boolean sequential,
|
||||
int updateInterval, int itemsToKeep) {
|
||||
long updateInterval, int itemsToKeep) {
|
||||
RemoteFeed rf = remoteFeeds.get(publisher);
|
||||
if (rf == null)
|
||||
return;
|
||||
|
@@ -219,7 +219,7 @@ public class FeedServlet extends HttpServlet {
|
||||
}
|
||||
boolean autoDownload = Boolean.valueOf(req.getParameter("autoDownload"));
|
||||
boolean sequential = Boolean.valueOf(req.getParameter("sequential"));
|
||||
int updateInterval = Integer.valueOf(req.getParameter("updateInterval")) * 60000;
|
||||
long updateInterval = Long.valueOf(req.getParameter("updateInterval")) * 60000;
|
||||
int itemsToKeep = Integer.valueOf(req.getParameter("itemsToKeep"));
|
||||
|
||||
feedManager.configure(host, autoDownload, sequential, updateInterval, itemsToKeep);
|
||||
|
@@ -52,7 +52,7 @@ public class MuWireServlet extends HttpServlet {
|
||||
"</head><body>\n" +
|
||||
"<header class=\"titlebar\">" +
|
||||
"<div class=\"title\">" +
|
||||
"<img src=\"images/muwire.png\" alt=\"\"><br>" +
|
||||
"<img src=\"images/muwire_logo.png\" alt=\"\"><br>" +
|
||||
_t("Welcome to MuWire") +
|
||||
"</div>" +
|
||||
"<div class=\"subtitle\"><br><br><br><br></div>" +
|
||||
|
@@ -12,24 +12,35 @@ import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.muwire.core.Core;
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
public class UploadServlet extends HttpServlet {
|
||||
|
||||
private UploadManager uploadManager;
|
||||
private BrowseManager browseManager;
|
||||
private Core core;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
List<UploadEntry> entries = new ArrayList<>();
|
||||
synchronized(uploadManager.getUploads()) {
|
||||
for(UploadManager.UploaderWrapper wrapper : uploadManager.getUploads()) {
|
||||
Persona downloader = wrapper.getUploader().getDownloaderPersona();
|
||||
UploadEntry entry = new UploadEntry(
|
||||
wrapper.getUploader().getName(),
|
||||
wrapper.getUploader().getProgress(),
|
||||
wrapper.getUploader().getDownloader(),
|
||||
wrapper.getUploader().getDownloaderPersona().toBase64(),
|
||||
wrapper.getUploader().getDonePieces(),
|
||||
wrapper.getUploader().getTotalPieces(),
|
||||
wrapper.getUploader().speed()
|
||||
wrapper.getUploader().speed(),
|
||||
wrapper.getUploader().isBrowseEnabled(),
|
||||
wrapper.getUploader().isFeedEnabled(),
|
||||
browseManager.isBrowsing(downloader),
|
||||
core.getFeedManager().getFeed(downloader) != null
|
||||
);
|
||||
entries.add(entry);
|
||||
}
|
||||
@@ -55,6 +66,8 @@ public class UploadServlet extends HttpServlet {
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
uploadManager = (UploadManager) config.getServletContext().getAttribute("uploadManager");
|
||||
browseManager = (BrowseManager) config.getServletContext().getAttribute("browseManager");
|
||||
core = (Core) config.getServletContext().getAttribute("core");
|
||||
}
|
||||
|
||||
|
||||
@@ -72,18 +85,26 @@ public class UploadServlet extends HttpServlet {
|
||||
private static class UploadEntry {
|
||||
private final String name;
|
||||
private final int progress;
|
||||
private final String downloader;
|
||||
private final String downloader, b64;
|
||||
private final int remotePieces;
|
||||
private final int totalPieces;
|
||||
private final int speed;
|
||||
private final boolean browse, feed;
|
||||
private final boolean browsing, subscribed;
|
||||
|
||||
UploadEntry(String name, int progress, String downloader, int remotePieces, int totalPieces, int speed) {
|
||||
UploadEntry(String name, int progress, String downloader, String b64, int remotePieces, int totalPieces, int speed,
|
||||
boolean browse, boolean feed, boolean browsing, boolean subscribed) {
|
||||
this.name = name;
|
||||
this.progress = progress;
|
||||
this.downloader = downloader;
|
||||
this.b64 = b64;
|
||||
this.remotePieces = progress == 100 ? remotePieces + 1 : remotePieces;
|
||||
this.totalPieces = totalPieces;
|
||||
this.speed = speed;
|
||||
this.browse = browse;
|
||||
this.feed = feed;
|
||||
this.browsing = browsing;
|
||||
this.subscribed = subscribed;
|
||||
}
|
||||
|
||||
void toXML(StringBuilder sb) {
|
||||
@@ -91,8 +112,13 @@ public class UploadServlet extends HttpServlet {
|
||||
sb.append("<Name>").append(Util.escapeHTMLinXML(name)).append("</Name>");
|
||||
sb.append("<Progress>").append(Util._t("{0}% of piece", String.valueOf(progress))).append("</Progress>");
|
||||
sb.append("<Downloader>").append(Util.escapeHTMLinXML(downloader)).append("</Downloader>");
|
||||
sb.append("<DownloaderB64>").append(b64).append("</DownloaderB64>");
|
||||
sb.append("<RemotePieces>").append(remotePieces).append("/").append(totalPieces).append("</RemotePieces>");
|
||||
sb.append("<Speed>").append(DataHelper.formatSize2Decimal(speed, false)).append("B/sec").append("</Speed>");
|
||||
sb.append("<Browse>").append(browse).append("</Browse>");
|
||||
sb.append("<Browsing>").append(browsing).append("</Browsing>");
|
||||
sb.append("<Feed>").append(feed).append("</Feed>");
|
||||
sb.append("<Subscribed>").append(subscribed).append("</Subscribed>");
|
||||
sb.append("</Upload>");
|
||||
}
|
||||
}
|
||||
@@ -119,6 +145,14 @@ public class UploadServlet extends HttpServlet {
|
||||
return Integer.compare(l.speed, r.speed);
|
||||
};
|
||||
|
||||
private static final Comparator<UploadEntry> BY_BROWSE = (l, r) -> {
|
||||
return Boolean.compare(l.browse, r.browse);
|
||||
};
|
||||
|
||||
private static final Comparator<UploadEntry> BY_FEED = (l, r) -> {
|
||||
return Boolean.compare(l.feed, r.feed);
|
||||
};
|
||||
|
||||
private static final ColumnComparators<UploadEntry> COMPARATORS = new ColumnComparators<>();
|
||||
static {
|
||||
COMPARATORS.add("Name", BY_NAME);
|
||||
@@ -126,6 +160,8 @@ public class UploadServlet extends HttpServlet {
|
||||
COMPARATORS.add("Downloader", BY_DOWNLOADER);
|
||||
COMPARATORS.add("Remote Pieces", BY_REMOTE_PIECES);
|
||||
COMPARATORS.add("Speed", BY_SPEED);
|
||||
COMPARATORS.add("Browse", BY_BROWSE);
|
||||
COMPARATORS.add("Feed", BY_FEED);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,8 +4,7 @@ import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import groovy.json.JsonOutput;
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
@@ -199,7 +198,7 @@ public class Util {
|
||||
if (!s.equals(tx))
|
||||
map.put(s, tx);
|
||||
}
|
||||
return JSONObject.toJSONString(map);
|
||||
return JsonOutput.toJson(map);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -42,7 +42,7 @@ class Downloader {
|
||||
}
|
||||
|
||||
getPauseResumeRetryBlock() {
|
||||
if (this.state == "FINISHED" || this.state == "CANCELLED")
|
||||
if (this.state == "FINISHED" || this.state == "CANCELLED" || this.state == "HOPELESS")
|
||||
return ""
|
||||
if (this.state == "FAILED") {
|
||||
var retryLink = new Link(_t("Retry"), "resumeDownload", [this.infoHash])
|
||||
@@ -105,8 +105,10 @@ function updateDownloader(infoHash) {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var path = this.responseXML.getElementsByTagName("Path")[0].childNodes[0].nodeValue
|
||||
var pieceSize = this.responseXML.getElementsByTagName("PieceSize")[0].childNodes[0].nodeValue
|
||||
var sequential = this.responseXML.getElementsByTagName("Sequential")[0].childNodes[0].nodeValue
|
||||
var knownSources = this.responseXML.getElementsByTagName("KnownSources")[0].childNodes[0].nodeValue
|
||||
var activeSources = this.responseXML.getElementsByTagName("ActiveSources")[0].childNodes[0].nodeValue
|
||||
var hopelessSources = this.responseXML.getElementsByTagName("HopelessSources")[0].childNodes[0].nodeValue
|
||||
var totalPieces = this.responseXML.getElementsByTagName("TotalPieces")[0].childNodes[0].nodeValue
|
||||
var donePieces = this.responseXML.getElementsByTagName("DonePieces")[0].childNodes[0].nodeValue
|
||||
|
||||
@@ -116,6 +118,10 @@ function updateDownloader(infoHash) {
|
||||
html += "<td>" + "<p align='right'>" + path + "</p>" + "</td>"
|
||||
html += "</tr>"
|
||||
html += "<tr>"
|
||||
html += "<td>" + _t("Sequential") + "</td>"
|
||||
html += "<td>" + "<p align='right'>" + sequential + "</p>" + "</td>"
|
||||
html += "</tr>"
|
||||
html += "<tr>"
|
||||
html += "<td>" + _t("Known Sources") + "</td>"
|
||||
html += "<td>" + "<p align='right'>" + knownSources + "</p>" + "</td>"
|
||||
html += "</tr>"
|
||||
@@ -124,6 +130,10 @@ function updateDownloader(infoHash) {
|
||||
html += "<td>" + "<p align='right'>" + activeSources + "</p>" + "</td>"
|
||||
html += "</tr>"
|
||||
html += "<tr>"
|
||||
html += "<td>" + _t("Hopeless Sources") + "</td>"
|
||||
html += "<td>" + "<p align='right'>" + hopelessSources + "</p>" + "</td>"
|
||||
html += "</tr>"
|
||||
html += "<tr>"
|
||||
html += "<td>" + _t("Piece Size") + "</td>"
|
||||
html += "<td>" + "<p align='right'>" + pieceSize + "</p>" + "</td>"
|
||||
html += "</tr>"
|
||||
|
@@ -4,7 +4,12 @@ class Upload {
|
||||
this.progress = xmlNode.getElementsByTagName("Progress")[0].childNodes[0].nodeValue;
|
||||
this.speed = xmlNode.getElementsByTagName("Speed")[0].childNodes[0].nodeValue;
|
||||
this.downloader = xmlNode.getElementsByTagName("Downloader")[0].childNodes[0].nodeValue
|
||||
this.downloaderB64 = xmlNode.getElementsByTagName("DownloaderB64")[0].childNodes[0].nodeValue
|
||||
this.remotePieces = xmlNode.getElementsByTagName("RemotePieces")[0].childNodes[0].nodeValue
|
||||
this.browse = xmlNode.getElementsByTagName("Browse")[0].childNodes[0].nodeValue
|
||||
this.browsing = xmlNode.getElementsByTagName("Browsing")[0].childNodes[0].nodeValue
|
||||
this.feed = xmlNode.getElementsByTagName("Feed")[0].childNodes[0].nodeValue
|
||||
this.subscribed = xmlNode.getElementsByTagName("Subscribed")[0].childNodes[0].nodeValue
|
||||
}
|
||||
|
||||
getMapping() {
|
||||
@@ -14,8 +19,29 @@ class Upload {
|
||||
mapping.set("Progress", this.progress)
|
||||
mapping.set("Downloader", this.downloader)
|
||||
mapping.set("Remote Pieces", this.remotePieces)
|
||||
mapping.set("Browse", this.getBrowseBlock())
|
||||
mapping.set("Feed", this.getFeedBlock())
|
||||
return mapping
|
||||
}
|
||||
|
||||
getBrowseBlock() {
|
||||
if (this.browse == "false")
|
||||
return ""
|
||||
if (this.browsing == "true")
|
||||
return "<a href='/MuWire/BrowseHost?currentHost=" + this.downloaderB64 + "'>" + _t("Browsing") + "</a>"
|
||||
var link = new Link(_t("Browse"), "browse", [this.downloaderB64])
|
||||
var block = "<span id='browse-link-" + this.downloaderB64 + "'>" + link.render() + "</span>"
|
||||
return block
|
||||
}
|
||||
|
||||
getFeedBlock() {
|
||||
if (this.feed == "false")
|
||||
return ""
|
||||
if (this.subscribed == "true")
|
||||
return "<a href='/MuWire/Feeds'>" + _t("Subscribed") + "</a>"
|
||||
var link = new Link(_t("Subscribe"), "subscribe", [this.downloaderB64])
|
||||
return "<span id='subscribe-link-" + this.downloaderB64 + "'>" + link.render() + "</span>"
|
||||
}
|
||||
}
|
||||
|
||||
function refreshUploads() {
|
||||
@@ -37,7 +63,7 @@ function refreshUploads() {
|
||||
newOrder = "ascending"
|
||||
else if (uploadsSortOrder == "ascending")
|
||||
newOrder = "descending"
|
||||
var table = new Table(["Name","Progress","Downloader","Remote Pieces","Speed"], "sortUploads", uploadsSortKey, newOrder, null)
|
||||
var table = new Table(["Name","Progress","Downloader","Browse","Feed","Remote Pieces","Speed"], "sortUploads", uploadsSortKey, newOrder, null)
|
||||
|
||||
for(i = 0; i < uploaderList.length; i++) {
|
||||
table.addRow(uploaderList[i].getMapping())
|
||||
@@ -60,6 +86,32 @@ function refreshUploads() {
|
||||
xmlhttp.send();
|
||||
}
|
||||
|
||||
function browse(host) {
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var linkSpan = document.getElementById("browse-link-"+host)
|
||||
linkSpan.innerHTML = "<a href='/MuWire/BrowseHost?currentHost=" + host+ "'>" + _t("Browsing") + "</a>"
|
||||
}
|
||||
}
|
||||
xmlhttp.open("POST", "/MuWire/Browse", true)
|
||||
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xmlhttp.send("action=browse&host="+host)
|
||||
}
|
||||
|
||||
function subscribe(host) {
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var linkSpan = document.getElementById("subscribe-link-" + host)
|
||||
linkSpan.innerHTML = "<a href='/MuWire/Feeds'>" + _t("Subscribed") + "</a>"
|
||||
}
|
||||
}
|
||||
xmlhttp.open("POST", "/MuWire/Feed", true)
|
||||
xmlhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xmlhttp.send("action=subscribe&host=" + host)
|
||||
}
|
||||
|
||||
function clear(ignored) {
|
||||
var xmlhttp = new XMLHttpRequest()
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
|
@@ -82,6 +82,13 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="downloadRetryInterval" class="right" value="<%= core.getMuOptions().getDownloadRetryInterval()%>"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tooltip"><%=Util._t("Give up on sources after this many failures (-1 means never)")%>
|
||||
<span class="tooltiptext"><%=Util._t("After how many download attempts MuWire should give up on the download source.")%></span>
|
||||
</div>
|
||||
</td>
|
||||
<td><p align="right"><input type="text" size="1" name="downloadMaxFailures" class="right" value="<%= core.getMuOptions().getDownloadMaxFailures()%>"></p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tooltip"><%=Util._t("Directory for downloaded files")%>
|
||||
<span class="tooltiptext"><%=Util._t("Where to save downloaded files. MuWire must be able to write to this location.")%></span>
|
||||
|
@@ -13,7 +13,7 @@
|
||||
<title>MuWire ${version}</title>
|
||||
<link href="i2pbote.css?${version}" rel="stylesheet" type="text/css">
|
||||
<link href="muwire.css?${version}" rel="stylesheet" type="text/css">
|
||||
<link rel="icon" type="image/png" href="images/muwire.png" />
|
||||
<link rel="icon" type="image/png" href="images/muwire_logo.png" />
|
||||
<script src="js/conncount.js?${version}" type="text/javascript"></script>
|
||||
<script src="js/translate.js?${version}" type="text/javascript"></script>
|
||||
<script src="js/accordion.js?${version}" type="text/javascript"></script>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<header class="titlebar">
|
||||
<div class="title">
|
||||
<a href="Home"><img src="images/muwire.png" alt=""></a>
|
||||
<a href="Home"><img src="images/muwire_logo.png" alt=""></a>
|
||||
<br><%=Util._t("Welcome to MuWire")%>
|
||||
</div>
|
||||
<div class="subtitle">
|
||||
|
Reference in New Issue
Block a user