Compare commits
158 Commits
docker-0.6
...
muwire-0.6
Author | SHA1 | Date | |
---|---|---|---|
e216678d9a | |||
4582cfa0b5 | |||
5ea64ecb90 | |||
bd9315954a | |||
83bdf76c08 | |||
a2ed308cd0 | |||
4020df0a77 | |||
6f4b4a2c2d | |||
83cd5e57a2 | |||
bb69535874 | |||
b7033e3277 | |||
4a9cea7d2e | |||
2aea965d72 | |||
9a6a1c8371 | |||
2042bfccb7 | |||
0d4b0df19d | |||
f363296ed1 | |||
8b33a5a284 | |||
7e70dbda86 | |||
c23db1293f | |||
54f4874ad6 | |||
886effa3b6 | |||
64d8b98ee2 | |||
2f2f620ae5 | |||
9a74cc5026 | |||
e3c5fe291d | |||
c77b848d44 | |||
cf5b5b164d | |||
3a340e40c8 | |||
e9eafe9380 | |||
270a8519b4 | |||
f8bbeb8ac0 | |||
2a4db868aa | |||
59219da1a2 | |||
a5fb824f71 | |||
68bc0bbf30 | |||
c6c1ac1d93 | |||
9646eadcb1 | |||
db91c9171d | |||
e542a50260 | |||
a9539c5999 | |||
d93dbbeb8b | |||
45659f0dca | |||
31a607ed7d | |||
7a6538beff | |||
509b5c3b99 | |||
fbb710cfc8 | |||
244015465a | |||
7285c12b97 | |||
aac259c0fe | |||
e3f58f8f5a | |||
045859fe04 | |||
3a8c66e857 | |||
773513b257 | |||
83fe2e9b75 | |||
455b0ea48e | |||
f4c96db841 | |||
fca8870283 | |||
3efb04d7bb | |||
62ce8ffa46 | |||
05b70a4573 | |||
b339784826 | |||
488f2964ee | |||
369779ab6a | |||
f5fe3da09d | |||
392deee34c | |||
7183f15c5c | |||
ca33535630 | |||
54abf82a91 | |||
14546737fd | |||
0f069f2fc9 | |||
9a44603d2f | |||
38a027c308 | |||
2ba81ccc84 | |||
0408349c07 | |||
95cb7f3214 | |||
69810d7203 | |||
f202fa34f3 | |||
c082e25c81 | |||
2bb07ff7b5 | |||
ff952890bc | |||
fc393619d8 | |||
2882c73876 | |||
cbb1de046b | |||
a272a45928 | |||
3133581363 | |||
c3d0dce281 | |||
8f710e68c2 | |||
15430d6c03 | |||
166b71f128 | |||
d724986ec6 | |||
198c5b5538 | |||
96d71ed08f | |||
bb7385688c | |||
e70bec3a51 | |||
ed04c40420 | |||
e9f00c2995 | |||
fd75d8229b | |||
0ff9ca8572 | |||
a07f01b641 | |||
b9333913c6 | |||
fcb5c573f9 | |||
1610766e01 | |||
e2a9db8056 | |||
a0cb214e2b | |||
f2bf921d4c | |||
aa0fcfb7de | |||
48cfce71a8 | |||
8798ea38e8 | |||
17cd60afe3 | |||
c10c1118e8 | |||
28425e93dc | |||
032338bb48 | |||
12e56b1c9a | |||
cc8801c48b | |||
57c75978b6 | |||
bfe198e1a6 | |||
8e274f940e | |||
9f3942c1c7 | |||
d60d57ee43 | |||
8e3a433afb | |||
49cf56fabb | |||
2b6565d107 | |||
366a2ef841 | |||
bcd24e56ac | |||
c7d1f0c23c | |||
853b9f67fc | |||
a505a2449a | |||
c11d81c6c3 | |||
ee5e90c4ab | |||
64d2a87d26 | |||
f0304dbe7d | |||
bdad8d9309 | |||
8c110bbae5 | |||
2cc1e384bc | |||
9337d1b74d | |||
16ed5dd346 | |||
7b55fc9ed8 | |||
d5c8050572 | |||
83546d68d2 | |||
a891c83518 | |||
aa56cc23c0 | |||
a2b37ef567 | |||
4bc04ae631 | |||
56da9a16b0 | |||
2935ee1a1d | |||
855183397b | |||
e27704c1af | |||
5c18b4a141 | |||
dcd233b7ad | |||
7cee8a28ba | |||
7446fc949a | |||
598ab90f63 | |||
043028c296 | |||
cd1757fac3 | |||
9d4b365e63 | |||
b12d57e30a | |||
f33d1b6db3 |
@ -4,7 +4,7 @@ FROM jlesage/baseimage-gui:alpine-3.10-glibc
|
||||
ARG DOCKER_IMAGE_VERSION=unknown
|
||||
|
||||
# JDK version
|
||||
ARG JDK=9
|
||||
ARG JDK=11
|
||||
|
||||
# Important directories
|
||||
ARG TMP_DIR=/muwire-tmp
|
||||
|
53
README.md
@ -1,3 +1,5 @@
|
||||
The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and issues at http://git.idk.i2p/zlatinb/muwire
|
||||
|
||||
# MuWire - Easy Anonymous File-Sharing
|
||||
|
||||
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
|
||||
@ -44,53 +46,7 @@ There is a Web-based UI under development. It is intended to be run as a plugin
|
||||
|
||||
## Docker
|
||||
|
||||
The Docker image is based on the wonderful work in [jlesage/docker-baseimage-gui].
|
||||
You can refer to it for environment variables to pass to the container.
|
||||
|
||||
If you don't want to use the image on dockerhub, build an image yourself.
|
||||
```bash
|
||||
MUWIRE_VERSION=`awk -F "=" '/^version/ { gsub(" ","") ; print $2}' gradle.properties`
|
||||
docker build -t muwire:latest,muwire:${MUWIRE_VERSION} .
|
||||
```
|
||||
|
||||
**Necessary configuration**
|
||||
|
||||
Since MuWire will be running in a container, it won't have direct access to the host's localhost.
|
||||
By default, it will be configured to use `172.17.0.1` as the target host.
|
||||
You'll need to open the I2CP port on that interface.
|
||||
If you're running I2P on the localhost, navigate to http://localhost:7657/configi2cp and make the necessary changes.
|
||||
|
||||
![i2cp_config.png]
|
||||
|
||||
Should you be using a different interface write an `i2p.properties` and then put that into the shared docker volume.
|
||||
|
||||
Example configuration file:
|
||||
```properties
|
||||
i2cp.tcp.host=112.13.0.1
|
||||
```
|
||||
|
||||
**Running**
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-p 5800:5800 \
|
||||
-v config:/muwire/.MuWire \
|
||||
-v incompletes:/incompletes \
|
||||
-v output:/output \
|
||||
--name muwire \
|
||||
zlatinb/muwire
|
||||
```
|
||||
|
||||
You will then be able to access the muwire GUI over a browser at http://localhost:5800
|
||||
|
||||
**Options**
|
||||
|
||||
|
||||
| Option | Description |
|
||||
|--------------|--------------------------------------------|
|
||||
|`-v config:/muwire/.MuWire`| This is where the `i2p.properties` and possibly other config should go |
|
||||
|`-v incompletes:/incompletes`| The `/incompletes` volume should be used to store MuWire's **incomplete** download/upload data \*|
|
||||
|`-v output:/output`| The `/output` volume should be used to store MuWire's download/upload data |
|
||||
MuWire is available as a Docker image. For more information see the [Docker] page.
|
||||
|
||||
## Translations
|
||||
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
|
||||
@ -111,6 +67,5 @@ You can find the full key at https://keybase.io/zlatinb
|
||||
[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
|
||||
[i2cp_config.png]: ./images/i2cp_config.png
|
||||
[muwire_incompletes.png]: ./images/muwire_incompletes.png
|
||||
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
|
||||
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui
|
||||
|
7
TODO.md
@ -19,7 +19,7 @@ This helps with scalability
|
||||
* Enum i18n
|
||||
* Ability to share trust list only with trusted users
|
||||
* Confidential files visible only to certain users
|
||||
* Public Feed feature
|
||||
* Advertise file feed and browseability in upload headers
|
||||
|
||||
### Chat
|
||||
* echo "unknown/innappropriate command" in the console
|
||||
@ -32,11 +32,14 @@ This helps with scalability
|
||||
### Swing GUI
|
||||
* I2P Status panel - display message when connected to external router
|
||||
* Search box - left identation
|
||||
* Ability to disable switching of tabs on actions
|
||||
* Ability to trust/browse/subscribe from uploads tab
|
||||
|
||||
### Web UI/Plugin
|
||||
* HTML 5 media players
|
||||
* Minimal dependency (break up groovy-all.jar)
|
||||
* Remove versions from jar names
|
||||
* Security: POST nonces, CSP headers
|
||||
* Upload files from browser to plugin via drag-and-drop
|
||||
* Check permissions, display better errors when sharing local folders
|
||||
|
||||
|
||||
|
@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
|
||||
class CliLanterna {
|
||||
private static final String MW_VERSION = "0.6.8"
|
||||
private static final String MW_VERSION = "0.6.12"
|
||||
|
||||
private static volatile Core core
|
||||
|
||||
|
@ -3,6 +3,7 @@ package com.muwire.clilanterna
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread
|
||||
import com.googlecode.lanterna.gui2.table.TableModel
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@ -72,7 +73,7 @@ class FilesModel {
|
||||
sharedFiles.each {
|
||||
long size = it.getCachedLength()
|
||||
boolean comment = it.comment != null
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
||||
boolean certified = core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
|
||||
String hits = String.valueOf(it.getHits())
|
||||
String downloaders = String.valueOf(it.getDownloaders().size())
|
||||
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)
|
||||
|
@ -21,7 +21,6 @@ import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
|
||||
class FilesView extends BasicWindow {
|
||||
private final FilesModel model
|
||||
@ -84,7 +83,6 @@ class FilesView extends BasicWindow {
|
||||
|
||||
Button unshareButton = new Button("Unshare", {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
core.eventBus.publish(new UIPersistFilesEvent())
|
||||
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
|
||||
} )
|
||||
Button addCommentButton = new Button("Add Comment", {
|
||||
|
@ -1,7 +1,12 @@
|
||||
package com.muwire.core
|
||||
|
||||
import com.muwire.core.files.PersisterDoneEvent
|
||||
import com.muwire.core.files.PersisterFolderService
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.ZipException
|
||||
|
||||
import com.muwire.core.chat.ChatDisconnectionEvent
|
||||
import com.muwire.core.chat.ChatManager
|
||||
@ -29,9 +34,18 @@ import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.filecert.UIFetchCertificatesEvent
|
||||
import com.muwire.core.filecert.UIImportCertificateEvent
|
||||
import com.muwire.core.filefeeds.FeedClient
|
||||
import com.muwire.core.filefeeds.FeedFetchEvent
|
||||
import com.muwire.core.filefeeds.FeedItemFetchedEvent
|
||||
import com.muwire.core.filefeeds.FeedManager
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.filefeeds.UIFilePublishedEvent
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
|
||||
import com.muwire.core.filefeeds.UIFeedDeletedEvent
|
||||
import com.muwire.core.filefeeds.UIFeedUpdateEvent
|
||||
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
import com.muwire.core.files.FileHashingEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.files.FileLoadedEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
@ -41,7 +55,7 @@ import com.muwire.core.files.HasherService
|
||||
import com.muwire.core.files.PersisterService
|
||||
import com.muwire.core.files.SideCarFileEvent
|
||||
import com.muwire.core.files.UICommentEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@ -74,10 +88,8 @@ import net.i2p.client.I2PClientFactory
|
||||
import net.i2p.client.I2PSession
|
||||
import net.i2p.client.streaming.I2PSocketManager
|
||||
import net.i2p.client.streaming.I2PSocketManagerFactory
|
||||
import net.i2p.client.streaming.I2PSocketOptions
|
||||
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.crypto.SigType
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.data.PrivateKey
|
||||
import net.i2p.data.Signature
|
||||
@ -100,14 +112,15 @@ public class Core {
|
||||
final TrustService trustService
|
||||
final TrustSubscriber trustSubscriber
|
||||
private final PersisterService persisterService
|
||||
private final HostCache hostCache
|
||||
private final ConnectionManager connectionManager
|
||||
private final PersisterFolderService persisterFolderService
|
||||
final HostCache hostCache
|
||||
final ConnectionManager connectionManager
|
||||
private final CacheClient cacheClient
|
||||
private final UpdateClient updateClient
|
||||
private final ConnectionAcceptor connectionAcceptor
|
||||
final ConnectionAcceptor connectionAcceptor
|
||||
private final ConnectionEstablisher connectionEstablisher
|
||||
private final HasherService hasherService
|
||||
private final DownloadManager downloadManager
|
||||
final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
final UploadManager uploadManager
|
||||
@ -115,6 +128,8 @@ public class Core {
|
||||
final CertificateManager certificateManager
|
||||
final ChatServer chatServer
|
||||
final ChatManager chatManager
|
||||
final FeedManager feedManager
|
||||
private final FeedClient feedClient
|
||||
|
||||
private final Router router
|
||||
|
||||
@ -131,7 +146,11 @@ public class Core {
|
||||
// Read defaults
|
||||
def defaultI2PFile = getClass()
|
||||
.getClassLoader().getResource("defaults/i2p.properties");
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
try {
|
||||
defaultI2PFile.withInputStream { i2pOptions.load(it) }
|
||||
} catch (ZipException mystery) {
|
||||
log.log(Level.SEVERE, "couldn't load default i2p properties", mystery)
|
||||
}
|
||||
|
||||
def i2pOptionsFile = new File(home, "i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
@ -193,7 +212,7 @@ public class Core {
|
||||
// options like tunnel length and quantity
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
socketManager = new I2PSocketManagerFactory().createDisconnectedManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
@ -259,7 +278,17 @@ public class Core {
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
|
||||
eventBus.register(UILoadedEvent.class, persisterService)
|
||||
eventBus.register(UIPersistFilesEvent.class, persisterService)
|
||||
|
||||
log.info "initializing folder persistence service"
|
||||
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
|
||||
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
|
||||
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileLoadedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileHashedEvent.class, persisterFolderService)
|
||||
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
|
||||
eventBus.register(UICommentEvent.class, persisterFolderService)
|
||||
eventBus.register(UIFilePublishedEvent.class, persisterFolderService)
|
||||
eventBus.register(UIFileUnpublishedEvent.class, persisterFolderService)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@ -302,6 +331,19 @@ public class Core {
|
||||
register(TrustEvent.class, chatServer)
|
||||
}
|
||||
|
||||
log.info("initializing feed manager")
|
||||
feedManager = new FeedManager(eventBus, home)
|
||||
eventBus.with {
|
||||
register(FeedItemFetchedEvent.class, feedManager)
|
||||
register(FeedFetchEvent.class, feedManager)
|
||||
register(UIFeedConfigurationEvent.class, feedManager)
|
||||
register(UIFeedDeletedEvent.class, feedManager)
|
||||
}
|
||||
|
||||
log.info("initializing feed client")
|
||||
feedClient = new FeedClient(i2pConnector, eventBus, me, feedManager)
|
||||
eventBus.register(UIFeedUpdateEvent.class, feedClient)
|
||||
|
||||
log.info "initializing results sender"
|
||||
ResultsSender resultsSender = new ResultsSender(eventBus, i2pConnector, me, props, certificateManager, chatServer)
|
||||
|
||||
@ -313,6 +355,7 @@ public class Core {
|
||||
log.info("initializing download manager")
|
||||
downloadManager = new DownloadManager(eventBus, trustService, meshManager, props, i2pConnector, home, me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadFeedItemEvent.class, downloadManager)
|
||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||
@ -321,7 +364,7 @@ public class Core {
|
||||
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
|
||||
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
|
||||
|
||||
log.info("initializing connection establisher")
|
||||
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
|
||||
@ -371,6 +414,7 @@ public class Core {
|
||||
}
|
||||
|
||||
public void startServices() {
|
||||
i2pSession.connect()
|
||||
hasherService.start()
|
||||
trustService.start()
|
||||
trustService.waitForLoad()
|
||||
@ -381,6 +425,8 @@ public class Core {
|
||||
connectionEstablisher.start()
|
||||
hostCache.waitForLoad()
|
||||
updateClient?.start()
|
||||
feedManager.start()
|
||||
feedClient.start()
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
@ -398,6 +444,8 @@ public class Core {
|
||||
trustService.stop()
|
||||
log.info("shutting down persister service")
|
||||
persisterService.stop()
|
||||
log.info("shutting down persisterFolder service")
|
||||
persisterFolderService.stop()
|
||||
log.info("shutting down download manager")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceptor")
|
||||
@ -412,6 +460,10 @@ public class Core {
|
||||
chatServer.stop()
|
||||
log.info("shutting down chat manager")
|
||||
chatManager.shutdown()
|
||||
log.info("shutting down feed manager")
|
||||
feedManager.stop()
|
||||
log.info("shutting down feed client")
|
||||
feedClient.stop()
|
||||
log.info("shutting down connection manager")
|
||||
connectionManager.shutdown()
|
||||
log.info("killing i2p session")
|
||||
@ -459,7 +511,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.6.8")
|
||||
Core core = new Core(props, home, "0.6.12")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@ -31,6 +31,16 @@ class MuWireSettings {
|
||||
boolean shareHiddenFiles
|
||||
boolean searchComments
|
||||
boolean browseFiles
|
||||
|
||||
boolean fileFeed
|
||||
boolean advertiseFeed
|
||||
boolean autoPublishSharedFiles
|
||||
boolean defaultFeedAutoDownload
|
||||
int defaultFeedUpdateInterval
|
||||
int defaultFeedItemsToKeep
|
||||
boolean defaultFeedSequential
|
||||
|
||||
|
||||
boolean startChatServer
|
||||
int maxChatConnections
|
||||
boolean advertiseChat
|
||||
@ -82,6 +92,16 @@ class MuWireSettings {
|
||||
outBw = Integer.valueOf(props.getProperty("outBw","128"))
|
||||
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
|
||||
browseFiles = Boolean.valueOf(props.getProperty("browseFiles","true"))
|
||||
|
||||
// feed settings
|
||||
fileFeed = Boolean.valueOf(props.getProperty("fileFeed","true"))
|
||||
advertiseFeed = Boolean.valueOf(props.getProperty("advertiseFeed","true"))
|
||||
autoPublishSharedFiles = Boolean.valueOf(props.getProperty("autoPublishSharedFiles", "false"))
|
||||
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"))
|
||||
|
||||
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","60"))
|
||||
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
|
||||
uploadSlotsPerUser = Integer.valueOf(props.getProperty("uploadSlotsPerUser","-1"))
|
||||
@ -137,6 +157,16 @@ class MuWireSettings {
|
||||
props.setProperty("outBw", String.valueOf(outBw))
|
||||
props.setProperty("searchComments", String.valueOf(searchComments))
|
||||
props.setProperty("browseFiles", String.valueOf(browseFiles))
|
||||
|
||||
// feed settings
|
||||
props.setProperty("fileFeed", String.valueOf(fileFeed))
|
||||
props.setProperty("advertiseFeed", String.valueOf(advertiseFeed))
|
||||
props.setProperty("autoPublishSharedFiles", String.valueOf(autoPublishSharedFiles))
|
||||
props.setProperty("defaultFeedAutoDownload", String.valueOf(defaultFeedAutoDownload))
|
||||
props.setProperty("defaultFeedItemsToKeep", String.valueOf(defaultFeedItemsToKeep))
|
||||
props.setProperty("defaultFeedSequential", String.valueOf(defaultFeedSequential))
|
||||
props.setProperty("defaultFeedUpdateInterval", String.valueOf(defaultFeedUpdateInterval))
|
||||
|
||||
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
|
||||
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
|
||||
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))
|
||||
|
@ -255,7 +255,6 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: make this mandatory at some point
|
||||
byte[] sig2 = null
|
||||
long queryTime = 0
|
||||
if (search.sig2 != null) {
|
||||
@ -278,8 +277,10 @@ abstract class Connection implements Closeable {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
log.info("no extended signature in query")
|
||||
return
|
||||
}
|
||||
|
||||
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
|
||||
searchHash : infohash,
|
||||
|
@ -15,9 +15,11 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.chat.ChatServer
|
||||
import com.muwire.core.filecert.Certificate
|
||||
import com.muwire.core.filecert.CertificateManager
|
||||
import com.muwire.core.filefeeds.FeedItems
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.hostcache.HostCache
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
@ -58,7 +60,7 @@ class ConnectionAcceptor {
|
||||
|
||||
private volatile shutdown
|
||||
|
||||
private volatile int browsed
|
||||
volatile int browsed
|
||||
|
||||
ConnectionAcceptor(EventBus eventBus, UltrapeerConnectionManager manager,
|
||||
MuWireSettings settings, I2PAcceptor acceptor, HostCache hostCache,
|
||||
@ -161,6 +163,9 @@ class ConnectionAcceptor {
|
||||
case (byte)'I':
|
||||
processIRC(e)
|
||||
break
|
||||
case (byte)'F':
|
||||
processFEED(e)
|
||||
break
|
||||
default:
|
||||
throw new Exception("Invalid read $read")
|
||||
}
|
||||
@ -310,6 +315,9 @@ class ConnectionAcceptor {
|
||||
boolean chat = false
|
||||
if (headers.containsKey('Chat'))
|
||||
chat = Boolean.parseBoolean(headers['Chat'])
|
||||
boolean feed = false
|
||||
if (headers.containsKey('Feed'))
|
||||
feed = Boolean.parseBoolean(headers['Feed'])
|
||||
|
||||
byte [] personaBytes = Base64.decode(headers['Sender'])
|
||||
Persona sender = new Persona(new ByteArrayInputStream(personaBytes))
|
||||
@ -329,6 +337,7 @@ class ConnectionAcceptor {
|
||||
def json = slurper.parse(payload)
|
||||
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||
results[i].chat = chat
|
||||
results[i].feed = feed
|
||||
}
|
||||
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||
} catch (IOException bad) {
|
||||
@ -374,13 +383,16 @@ class ConnectionAcceptor {
|
||||
boolean chat = chatServer.running.get() && 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))
|
||||
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
sharedFiles.each {
|
||||
it.hit(browser, System.currentTimeMillis(), "Browse Host");
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
@ -524,5 +536,58 @@ class ConnectionAcceptor {
|
||||
throw new Exception("Invalid IRC connection")
|
||||
chatServer.handle(e)
|
||||
}
|
||||
|
||||
private void processFEED(Endpoint e) {
|
||||
try {
|
||||
byte[] EED = new byte[5];
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(EED);
|
||||
if (EED != "EED\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new Exception("Invalid FEED connection")
|
||||
|
||||
OutputStream os = e.getOutputStream()
|
||||
|
||||
Map<String, String> headers = DataUtil.readAllHeaders(dis)
|
||||
if (!headers.containsKey("Persona"))
|
||||
throw new Exception("Persona header missing")
|
||||
Persona requestor = new Persona(new ByteArrayInputStream(Base64.decode(headers['Persona'])))
|
||||
if (requestor.destination != e.destination)
|
||||
throw new Exception("Requestor persona mismatch")
|
||||
|
||||
if (!settings.fileFeed) {
|
||||
os.write("403 Not Allowed\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
long timestamp = 0
|
||||
if (headers.containsKey("Timestamp")) {
|
||||
timestamp = Long.parseLong(headers['Timestamp'])
|
||||
}
|
||||
|
||||
List<SharedFile> published = fileManager.getPublishedSince(timestamp)
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Count: ${published.size()}\r\n".getBytes(StandardCharsets.US_ASCII));
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
final long now = System.currentTimeMillis();
|
||||
published.each {
|
||||
it.hit(requestor, now, "Feed Update");
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = FeedItems.sharedFileToObj(it, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
dos.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
}
|
||||
dos.flush()
|
||||
dos.close()
|
||||
} finally {
|
||||
e.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.mesh.Mesh
|
||||
@ -62,11 +63,6 @@ public class DownloadManager {
|
||||
|
||||
|
||||
public void onUIDownloadEvent(UIDownloadEvent e) {
|
||||
|
||||
File incompletes = muSettings.incompleteLocation
|
||||
if (incompletes == null)
|
||||
incompletes = new File(home, "incompletes")
|
||||
incompletes.mkdirs()
|
||||
|
||||
def size = e.result[0].size
|
||||
def infohash = e.result[0].infohash
|
||||
@ -79,12 +75,29 @@ public class DownloadManager {
|
||||
destinations.addAll(e.sources)
|
||||
destinations.remove(me.destination)
|
||||
|
||||
Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
|
||||
doDownload(infohash, e.target, size, pieceSize, e.sequential, destinations)
|
||||
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes, pieces)
|
||||
downloaders.put(infohash, downloader)
|
||||
}
|
||||
|
||||
public void onUIDownloadFeedItemEvent(UIDownloadFeedItemEvent e) {
|
||||
Set<Destination> singleSource = new HashSet<>()
|
||||
singleSource.add(e.item.getPublisher().getDestination())
|
||||
doDownload(e.item.getInfoHash(), e.target, e.item.getSize(), e.item.getPieceSize(),
|
||||
e.sequential, singleSource)
|
||||
}
|
||||
|
||||
private void doDownload(InfoHash infoHash, File target, long size, int pieceSize,
|
||||
boolean sequential, Set<Destination> destinations) {
|
||||
File incompletes = muSettings.incompleteLocation
|
||||
if (incompletes == null)
|
||||
incompletes = new File(home, "incompletes")
|
||||
incompletes.mkdirs()
|
||||
|
||||
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
|
||||
def downloader = new Downloader(eventBus, this, me, target, size,
|
||||
infoHash, pieceSize, connector, destinations,
|
||||
incompletes, pieces)
|
||||
downloaders.put(infoHash, downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
@ -229,4 +242,8 @@ public class DownloadManager {
|
||||
downloaders.values().each { it.stop() }
|
||||
Downloader.executorService.shutdownNow()
|
||||
}
|
||||
|
||||
public boolean isDownloading(InfoHash infoHash) {
|
||||
downloaders.containsKey(infoHash)
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ public class Downloader {
|
||||
long dataRead = dataSinceLastRead.getAndSet(0)
|
||||
long now = System.currentTimeMillis()
|
||||
if (now > lastSpeedRead)
|
||||
currSpeed = (int) (dataRead * 1000.0 / (now - lastSpeedRead))
|
||||
currSpeed = (int) (dataRead * 1000.0d / (now - lastSpeedRead))
|
||||
lastSpeedRead = now
|
||||
}
|
||||
|
||||
@ -405,8 +405,9 @@ public class Downloader {
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this))
|
||||
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash().getRoot(), pieceSizePow2, successfulDestinations),
|
||||
downloader : Downloader.this,
|
||||
infoHash: getInfoHash()))
|
||||
|
||||
}
|
||||
endpoint?.close()
|
||||
|
@ -6,6 +6,7 @@ class Pieces {
|
||||
private final float ratio
|
||||
private final Random random = new Random()
|
||||
private final Map<Integer,Integer> partials = new HashMap<>()
|
||||
private int cachedDone;
|
||||
|
||||
Pieces(int nPieces) {
|
||||
this(nPieces, 1.0f)
|
||||
@ -78,6 +79,7 @@ class Pieces {
|
||||
if (piece >= nPieces)
|
||||
throw new IllegalArgumentException("invalid piece marked as downloaded? $piece/$nPieces")
|
||||
done.set(piece)
|
||||
cachedDone = done.cardinality();
|
||||
claimed.set(piece)
|
||||
partials.remove(piece)
|
||||
}
|
||||
@ -91,11 +93,11 @@ class Pieces {
|
||||
}
|
||||
|
||||
synchronized boolean isComplete() {
|
||||
done.cardinality() == nPieces
|
||||
cachedDone == nPieces
|
||||
}
|
||||
|
||||
synchronized int donePieces() {
|
||||
done.cardinality()
|
||||
cachedDone
|
||||
}
|
||||
|
||||
synchronized boolean isDownloaded(int piece) {
|
||||
@ -104,6 +106,7 @@ class Pieces {
|
||||
|
||||
synchronized void clearAll() {
|
||||
done.clear()
|
||||
cachedDone = 0
|
||||
claimed.clear()
|
||||
partials.clear()
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ class CertificateManager {
|
||||
}
|
||||
|
||||
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
|
||||
InfoHash infoHash = e.sharedFile.getInfoHash()
|
||||
InfoHash infoHash = new InfoHash(e.sharedFile.getRoot())
|
||||
String name = e.sharedFile.getFile().getName()
|
||||
long timestamp = System.currentTimeMillis()
|
||||
|
||||
|
110
core/src/main/groovy/com/muwire/core/filefeeds/FeedClient.groovy
Normal file
@ -0,0 +1,110 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import java.util.logging.Level
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.connection.Endpoint
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
@Log
|
||||
class FeedClient {
|
||||
|
||||
private final I2PConnector connector
|
||||
private final EventBus eventBus
|
||||
private final Persona me
|
||||
private final FeedManager feedManager
|
||||
|
||||
private final ExecutorService feedFetcher = Executors.newCachedThreadPool()
|
||||
private final Timer feedUpdater = new Timer("feed-updater", true)
|
||||
|
||||
FeedClient(I2PConnector connector, EventBus eventBus, Persona me, FeedManager feedManager) {
|
||||
this.connector = connector
|
||||
this.eventBus = eventBus
|
||||
this.me = me
|
||||
this.feedManager = feedManager
|
||||
}
|
||||
|
||||
private void start() {
|
||||
feedUpdater.schedule({updateAnyFeeds()} as TimerTask, 60000, 60000)
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
feedUpdater.cancel()
|
||||
feedFetcher.shutdown()
|
||||
}
|
||||
|
||||
private void updateAnyFeeds() {
|
||||
feedManager.getFeedsToUpdate().each { feed ->
|
||||
feedFetcher.execute({updateFeed(feed)} as Runnable)
|
||||
}
|
||||
}
|
||||
|
||||
void onUIFeedUpdateEvent(UIFeedUpdateEvent e) {
|
||||
Feed feed = feedManager.getFeed(e.host)
|
||||
if (feed == null) {
|
||||
log.severe("UI request to update non-existent feed " + e.host.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
|
||||
feedFetcher.execute({updateFeed(feed)} as Runnable)
|
||||
}
|
||||
|
||||
private void updateFeed(Feed feed) {
|
||||
log.info("updating feed " + feed.getPublisher().getHumanReadableName())
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.CONNECTING))
|
||||
feed.setLastUpdateAttempt(System.currentTimeMillis())
|
||||
endpoint = connector.connect(feed.getPublisher().getDestination())
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("FEED\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Persona:${me.toBase64()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Timestamp:${feed.getLastUpdated()}\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
InputStream is = endpoint.getInputStream()
|
||||
String code = DataUtil.readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("Invalid code $code")
|
||||
|
||||
// parse all headers
|
||||
Map<String,String> headers = DataUtil.readAllHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Count"))
|
||||
throw new IOException("No count header")
|
||||
|
||||
int items = Integer.parseInt(headers['Count'])
|
||||
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FETCHING, totalItems: items))
|
||||
|
||||
JsonSlurper slurper = new JsonSlurper()
|
||||
DataInputStream dis = new DataInputStream(new GZIPInputStream(is))
|
||||
for (int i = 0; i < items; i++) {
|
||||
int size = dis.readUnsignedShort()
|
||||
byte [] tmp = new byte[size]
|
||||
dis.readFully(tmp)
|
||||
def json = slurper.parse(tmp)
|
||||
FeedItem item = FeedItems.objToFeedItem(json, feed.getPublisher())
|
||||
eventBus.publish(new FeedItemFetchedEvent(item: item))
|
||||
}
|
||||
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FINISHED))
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.WARNING, "Feed update failed", bad)
|
||||
eventBus.publish(new FeedFetchEvent(host : feed.getPublisher(), status : FeedFetchStatus.FAILED))
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class FeedFetchEvent extends Event {
|
||||
Persona host
|
||||
FeedFetchStatus status
|
||||
int totalItems
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedItemFetchedEvent extends Event {
|
||||
FeedItem item
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedItemLoadedEvent extends Event {
|
||||
FeedItem item
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
class FeedItems {
|
||||
|
||||
public static def sharedFileToObj(SharedFile sf, int certificates) {
|
||||
def json = [:]
|
||||
json.type = "FeedItem"
|
||||
json.version = 1
|
||||
json.name = Base64.encode(DataUtil.encodei18nString(sf.getFile().getName()))
|
||||
json.infoHash = Base64.encode(sf.getRoot())
|
||||
json.size = sf.getCachedLength()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
|
||||
if (sf.getComment() != null)
|
||||
json.comment = sf.getComment()
|
||||
|
||||
json.certificates = certificates
|
||||
|
||||
json.timestamp = sf.getPublishedTimestamp()
|
||||
|
||||
json
|
||||
}
|
||||
|
||||
public static FeedItem objToFeedItem(def obj, Persona publisher) throws InvalidFeedItemException {
|
||||
if (obj.timestamp == null)
|
||||
throw new InvalidFeedItemException("No timestamp");
|
||||
if (obj.name == null)
|
||||
throw new InvalidFeedItemException("No name");
|
||||
if (obj.size == null || obj.size <= 0 || obj.size > FileHasher.MAX_SIZE)
|
||||
throw new InvalidFeedItemException("length missing or invalid ${obj.size}")
|
||||
if (obj.pieceSize == null || obj.pieceSize < FileHasher.MIN_PIECE_SIZE_POW2 || obj.pieceSize > FileHasher.MAX_PIECE_SIZE_POW2)
|
||||
throw new InvalidFeedItemException("piece size missing or invalid ${obj.pieceSize}")
|
||||
if (obj.infoHash == null)
|
||||
throw new InvalidFeedItemException("Infohash missing")
|
||||
|
||||
|
||||
InfoHash infoHash
|
||||
try {
|
||||
infoHash = new InfoHash(Base64.decode(obj.infoHash))
|
||||
} catch (Exception bad) {
|
||||
throw new InvalidFeedItemException("Invalid infohash", bad)
|
||||
}
|
||||
|
||||
String name
|
||||
try {
|
||||
name = DataUtil.readi18nString(Base64.decode(obj.name))
|
||||
} catch (Exception bad) {
|
||||
throw new InvalidFeedItemException("Invalid name", bad)
|
||||
}
|
||||
|
||||
int certificates = 0
|
||||
if (obj.certificates != null)
|
||||
certificates = obj.certificates
|
||||
|
||||
new FeedItem(publisher, obj.timestamp, name, obj.size, obj.pieceSize, infoHash, certificates, obj.comment)
|
||||
}
|
||||
|
||||
public static def feedItemToObj(FeedItem item) {
|
||||
def json = [:]
|
||||
json.type = "FeedItem"
|
||||
json.version = 1
|
||||
json.name = Base64.encode(DataUtil.encodei18nString(item.getName()))
|
||||
json.infoHash = Base64.encode(item.getInfoHash().getRoot())
|
||||
json.size = item.getSize()
|
||||
json.pieceSize = item.getPieceSize()
|
||||
json.timestamp = item.getTimestamp()
|
||||
json.certificates = item.getCertificates()
|
||||
json.comment = item.getComment()
|
||||
json
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class FeedLoadedEvent extends Event {
|
||||
Feed feed
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.Persona
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
@Log
|
||||
class FeedManager {
|
||||
|
||||
private final EventBus eventBus
|
||||
private final File metadataFolder, itemsFolder
|
||||
private final Map<Persona, Feed> feeds = new ConcurrentHashMap<>()
|
||||
private final Map<Persona, Set<FeedItem>> feedItems = new ConcurrentHashMap<>()
|
||||
|
||||
private final ExecutorService persister = Executors.newSingleThreadExecutor({r ->
|
||||
new Thread(r, "feed persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
|
||||
FeedManager(EventBus eventBus, File home) {
|
||||
this.eventBus = eventBus
|
||||
File feedsFolder = new File(home, "filefeeds")
|
||||
if (!feedsFolder.exists())
|
||||
feedsFolder.mkdir()
|
||||
this.metadataFolder = new File(feedsFolder, "metadata")
|
||||
if (!metadataFolder.exists())
|
||||
metadataFolder.mkdir()
|
||||
this.itemsFolder = new File(feedsFolder, "items")
|
||||
if (!itemsFolder.exists())
|
||||
itemsFolder.mkdir()
|
||||
}
|
||||
|
||||
public Feed getFeed(Persona persona) {
|
||||
feeds.get(persona)
|
||||
}
|
||||
|
||||
public Set<FeedItem> getFeedItems(Persona persona) {
|
||||
feedItems.getOrDefault(persona, Collections.emptySet())
|
||||
}
|
||||
|
||||
public List<Feed> getFeedsToUpdate() {
|
||||
long now = System.currentTimeMillis()
|
||||
feeds.values().stream().
|
||||
filter({Feed f -> !f.getStatus().isActive()}).
|
||||
filter({Feed f -> f.getLastUpdateAttempt() + f.getUpdateInterval() <= now})
|
||||
.collect(Collectors.toList())
|
||||
}
|
||||
|
||||
void start() {
|
||||
log.info("starting feed manager")
|
||||
persister.submit({loadFeeds()} as Runnable)
|
||||
persister.submit({loadItems()} as Runnable)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
persister.shutdown()
|
||||
}
|
||||
|
||||
private void loadFeeds() {
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(metadataFolder.toPath()).
|
||||
filter( { it.getFileName().toString().endsWith(".json")}).
|
||||
forEach( {
|
||||
def parsed = slurper.parse(it.toFile())
|
||||
Persona publisher = new Persona(new ByteArrayInputStream(Base64.decode(parsed.publisher)))
|
||||
Feed feed = new Feed(publisher)
|
||||
feed.setUpdateInterval(parsed.updateInterval)
|
||||
feed.setLastUpdated(parsed.lastUpdated)
|
||||
feed.setLastUpdateAttempt(parsed.lastUpdateAttempt)
|
||||
feed.setItemsToKeep(parsed.itemsToKeep)
|
||||
feed.setAutoDownload(parsed.autoDownload)
|
||||
feed.setSequential(parsed.sequential)
|
||||
|
||||
feed.setStatus(FeedFetchStatus.IDLE)
|
||||
|
||||
feeds.put(feed.getPublisher(), feed)
|
||||
|
||||
eventBus.publish(new FeedLoadedEvent(feed : feed))
|
||||
})
|
||||
}
|
||||
|
||||
private void loadItems() {
|
||||
def slurper = new JsonSlurper()
|
||||
feeds.keySet().each { persona ->
|
||||
File itemsFile = getItemsFile(feeds[persona])
|
||||
if (!itemsFile.exists())
|
||||
return // no items yet?
|
||||
itemsFile.eachLine { line ->
|
||||
def parsed = slurper.parseText(line)
|
||||
FeedItem item = FeedItems.objToFeedItem(parsed, persona)
|
||||
Set<FeedItem> items = feedItems.get(persona)
|
||||
if (items == null) {
|
||||
items = new ConcurrentHashSet<>()
|
||||
feedItems.put(persona, items)
|
||||
}
|
||||
items.add(item)
|
||||
eventBus.publish(new FeedItemLoadedEvent(item : item))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onFeedItemFetchedEvent(FeedItemFetchedEvent e) {
|
||||
Set<FeedItem> set = feedItems.get(e.item.getPublisher())
|
||||
if (set == null) {
|
||||
set = new ConcurrentHashSet<>()
|
||||
feedItems.put(e.getItem().getPublisher(), set)
|
||||
}
|
||||
set.add(e.item)
|
||||
}
|
||||
|
||||
void onFeedFetchEvent(FeedFetchEvent e) {
|
||||
|
||||
Feed feed = feeds.get(e.host)
|
||||
if (feed == null) {
|
||||
log.severe("Fetching non-existent feed " + e.host.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
|
||||
feed.setStatus(e.status)
|
||||
|
||||
if (e.status.isActive())
|
||||
return
|
||||
|
||||
if (e.status == FeedFetchStatus.FINISHED) {
|
||||
feed.setStatus(FeedFetchStatus.IDLE)
|
||||
feed.setLastUpdated(e.getTimestamp())
|
||||
}
|
||||
// save feed items, then save feed. This will save partial fetches too
|
||||
// which is ok because the items are stored in a Set
|
||||
persister.submit({saveFeedItems(e.host)} as Runnable)
|
||||
persister.submit({saveFeedMetadata(feed)} as Runnable)
|
||||
}
|
||||
|
||||
void onUIFeedConfigurationEvent(UIFeedConfigurationEvent e) {
|
||||
feeds.put(e.feed.getPublisher(), e.feed)
|
||||
persister.submit({saveFeedMetadata(e.feed)} as Runnable)
|
||||
}
|
||||
|
||||
void onUIFeedDeletedEvent(UIFeedDeletedEvent e) {
|
||||
Feed f = feeds.get(e.host)
|
||||
if (f == null) {
|
||||
log.severe("Deleting a non-existing feed " + e.host.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
persister.submit({deleteFeed(f)} as Runnable)
|
||||
}
|
||||
|
||||
private void saveFeedItems(Persona publisher) {
|
||||
Set<FeedItem> set = feedItems.get(publisher)
|
||||
if (set == null)
|
||||
return // can happen if nothing was published
|
||||
|
||||
Feed feed = feeds[publisher]
|
||||
if (feed == null) {
|
||||
log.severe("Persisting items for non-existing feed " + publisher.getHumanReadableName())
|
||||
return
|
||||
}
|
||||
|
||||
if (feed.getItemsToKeep() == 0)
|
||||
return
|
||||
|
||||
List<FeedItem> list = new ArrayList<>(set)
|
||||
if (feed.getItemsToKeep() > 0 && list.size() > feed.getItemsToKeep()) {
|
||||
log.info("will persist ${feed.getItemsToKeep()}/${list.size()} items")
|
||||
list.sort({l, r ->
|
||||
Long.compare(r.getTimestamp(), l.getTimestamp())
|
||||
} as Comparator<FeedItem>)
|
||||
list = list[0..feed.getItemsToKeep() - 1]
|
||||
}
|
||||
|
||||
|
||||
File itemsFile = getItemsFile(feed)
|
||||
itemsFile.withPrintWriter { writer ->
|
||||
list.each { item ->
|
||||
def obj = FeedItems.feedItemToObj(item)
|
||||
def json = JsonOutput.toJson(obj)
|
||||
writer.println(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFeedMetadata(Feed feed) {
|
||||
File metadataFile = getMetadataFile(feed)
|
||||
metadataFile.withPrintWriter { writer ->
|
||||
def json = [:]
|
||||
json.publisher = feed.getPublisher().toBase64()
|
||||
json.itemsToKeep = feed.getItemsToKeep()
|
||||
json.lastUpdated = feed.getLastUpdated()
|
||||
json.updateInterval = feed.getUpdateInterval()
|
||||
json.autoDownload = feed.isAutoDownload()
|
||||
json.sequential = feed.isSequential()
|
||||
json.lastUpdateAttempt = feed.getLastUpdateAttempt()
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println(json)
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteFeed(Feed feed) {
|
||||
feeds.remove(feed.getPublisher())
|
||||
feedItems.remove(feed.getPublisher())
|
||||
getItemsFile(feed).delete()
|
||||
getMetadataFile(feed).delete()
|
||||
}
|
||||
|
||||
private File getItemsFile(Feed feed) {
|
||||
return new File(itemsFolder, feed.getPublisher().destination.toBase32() + ".json")
|
||||
}
|
||||
|
||||
private File getMetadataFile(Feed feed) {
|
||||
return new File(metadataFolder, feed.getPublisher().destination.toBase32() + ".json")
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadFeedItemEvent extends Event {
|
||||
FeedItem item
|
||||
File target
|
||||
boolean sequential
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Emitted when configuration of a feed changes.
|
||||
* The object should already contain the updated values.
|
||||
*/
|
||||
class UIFeedConfigurationEvent extends Event {
|
||||
Feed feed
|
||||
boolean newFeed
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIFeedDeletedEvent extends Event {
|
||||
Persona host
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.Persona
|
||||
|
||||
class UIFeedUpdateEvent extends Event {
|
||||
Persona host
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UIFilePublishedEvent extends Event {
|
||||
SharedFile sf
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.muwire.core.filefeeds
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class UIFileUnpublishedEvent extends Event {
|
||||
SharedFile sf
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.util.DataUtil
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
import java.util.stream.Collectors
|
||||
|
||||
abstract class BasePersisterService extends Service{
|
||||
|
||||
protected static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
|
||||
|
||||
}
|
||||
|
||||
protected static FileLoadedEvent fromJsonLite(json) {
|
||||
if (json.file == null || json.length == null || json.root == null)
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
byte[] root = Base64.decode(json.root)
|
||||
InfoHash ih = new InfoHash(root)
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
boolean published = false
|
||||
long publishedTimestamp = -1
|
||||
if (json.published != null && json.published) {
|
||||
published = true
|
||||
publishedTimestamp = json.publishedTimestamp
|
||||
}
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
|
||||
if (published)
|
||||
df.publish(publishedTimestamp)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (published)
|
||||
sf.publish(publishedTimestamp)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
|
||||
}
|
||||
|
||||
protected static toJson(SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
json.root = Base64.encode(sf.getRoot())
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
json.comment = sf.getComment()
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (!sf.searches.isEmpty()) {
|
||||
Set searchers = new HashSet<>()
|
||||
sf.searches.each {
|
||||
def search = [:]
|
||||
if (it.searcher != null)
|
||||
search.searcher = it.searcher.toBase64()
|
||||
search.timestamp = it.timestamp
|
||||
search.query = it.query
|
||||
searchers.add(search)
|
||||
}
|
||||
json.searchers = searchers
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
if (sf.isPublished()) {
|
||||
json.published = true
|
||||
json.publishedTimestamp = sf.getPublishedTimestamp()
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
}
|
@ -4,8 +4,9 @@ import com.muwire.core.Event
|
||||
|
||||
class DirectoryUnsharedEvent extends Event {
|
||||
File directory
|
||||
boolean deleted
|
||||
|
||||
public String toString() {
|
||||
super.toString() + " unshared directory "+ directory.toString()
|
||||
super.toString() + " unshared directory "+ directory.toString() + " deleted $deleted"
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +133,10 @@ class DirectoryWatcher {
|
||||
SharedFile sf = fileManager.fileToSharedFile.get(f)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf, deleted : true))
|
||||
else if (muOptions.watchedDirectories.contains(f.toString()))
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : f, deleted : true))
|
||||
else
|
||||
log.fine("Entry was not relevant");
|
||||
}
|
||||
|
||||
private static File join(Path parent, Path path) {
|
||||
|
@ -2,6 +2,7 @@ package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
import net.i2p.data.Destination
|
||||
@ -9,4 +10,5 @@ import net.i2p.data.Destination
|
||||
class FileDownloadedEvent extends Event {
|
||||
Downloader downloader
|
||||
DownloadedFile downloadedFile
|
||||
InfoHash infoHash
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileHashedEvent extends Event {
|
||||
|
||||
SharedFile sharedFile
|
||||
InfoHash infoHash
|
||||
String error
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + " sharedFile " + sharedFile?.file.getAbsolutePath() + " error: $error"
|
||||
super.toString() + " sharedFile " + sharedFile?.file?.getAbsolutePath() + " error: $error"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
class FileLoadedEvent extends Event {
|
||||
|
||||
SharedFile loadedFile
|
||||
InfoHash infoHash
|
||||
String source
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.util.stream.Collectors
|
||||
import java.util.stream.Stream
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
@ -25,6 +28,7 @@ class FileManager {
|
||||
final Map<String, Set<File>> commentToFile = new HashMap<>()
|
||||
final SearchIndex index = new SearchIndex()
|
||||
final FileTree<Void> negativeTree = new FileTree<>()
|
||||
final FileTree<SharedFile> positiveTree = new FileTree<>()
|
||||
final Set<File> sideCarFiles = new HashSet<>()
|
||||
|
||||
FileManager(EventBus eventBus, MuWireSettings settings) {
|
||||
@ -75,7 +79,7 @@ class FileManager {
|
||||
|
||||
private void addToIndex(SharedFile sf) {
|
||||
log.info("Adding shared file " + sf.getFile())
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
InfoHash infoHash = new InfoHash(sf.getRoot())
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing == null) {
|
||||
log.info("adding new root")
|
||||
@ -84,6 +88,7 @@ class FileManager {
|
||||
}
|
||||
existing.add(sf)
|
||||
fileToSharedFile.put(sf.file, sf)
|
||||
positiveTree.add(sf.file, sf);
|
||||
|
||||
negativeTree.remove(sf.file)
|
||||
String parent = sf.getFile().getParent()
|
||||
@ -117,7 +122,7 @@ class FileManager {
|
||||
|
||||
void onFileUnsharedEvent(FileUnsharedEvent e) {
|
||||
SharedFile sf = e.unsharedFile
|
||||
InfoHash infoHash = sf.getInfoHash()
|
||||
InfoHash infoHash = new InfoHash(sf.getRoot())
|
||||
Set<SharedFile> existing = rootToFiles.get(infoHash)
|
||||
if (existing != null) {
|
||||
existing.remove(sf)
|
||||
@ -127,6 +132,7 @@ class FileManager {
|
||||
}
|
||||
|
||||
fileToSharedFile.remove(sf.file)
|
||||
positiveTree.remove(sf.file)
|
||||
if (!e.deleted && negativeTree.fileToNode.containsKey(sf.file.getParentFile())) {
|
||||
negativeTree.add(sf.file,null)
|
||||
saveNegativeTree()
|
||||
@ -190,6 +196,10 @@ class FileManager {
|
||||
Set<SharedFile> getSharedFiles(byte []root) {
|
||||
return rootToFiles.get(new InfoHash(root))
|
||||
}
|
||||
|
||||
boolean isShared(InfoHash infoHash) {
|
||||
rootToFiles.containsKey(infoHash)
|
||||
}
|
||||
|
||||
void onSearchEvent(SearchEvent e) {
|
||||
// hash takes precedence
|
||||
@ -239,14 +249,26 @@ class FileManager {
|
||||
void onDirectoryUnsharedEvent(DirectoryUnsharedEvent e) {
|
||||
negativeTree.remove(e.directory)
|
||||
saveNegativeTree()
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
SharedFile sf = fileToSharedFile.get(it)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
if (!e.deleted) {
|
||||
e.directory.listFiles().each {
|
||||
if (it.isDirectory())
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it))
|
||||
else {
|
||||
SharedFile sf = fileToSharedFile.get(it)
|
||||
if (sf != null)
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def cb = new DirDeletionCallback()
|
||||
positiveTree.traverse(e.directory, cb)
|
||||
positiveTree.remove(e.directory)
|
||||
cb.unsharedFiles.each {
|
||||
eventBus.publish(new FileUnsharedEvent(unsharedFile : it, deleted: true))
|
||||
}
|
||||
cb.subDirs.each {
|
||||
eventBus.publish(new DirectoryUnsharedEvent(directory : it, deleted : true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,4 +276,34 @@ class FileManager {
|
||||
settings.negativeFileTree.clear()
|
||||
settings.negativeFileTree.addAll(negativeTree.fileToNode.keySet().collect { it.getAbsolutePath() })
|
||||
}
|
||||
|
||||
public List<SharedFile> getPublishedSince(long timestamp) {
|
||||
synchronized(fileToSharedFile) {
|
||||
fileToSharedFile.values().stream().
|
||||
filter({sf -> sf.isPublished()}).
|
||||
filter({sf -> sf.getPublishedTimestamp() >= timestamp}).
|
||||
collect(Collectors.toList())
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirDeletionCallback implements FileTreeCallback<SharedFile> {
|
||||
|
||||
final List<File> subDirs = new ArrayList<>()
|
||||
final List<SharedFile> unsharedFiles = new ArrayList<>()
|
||||
|
||||
@Override
|
||||
public void onDirectoryEnter(File file) {
|
||||
subDirs.add(file)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDirectoryLeave() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFile(File file, SharedFile value) {
|
||||
unsharedFiles << value
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ class FileTree<T> {
|
||||
if (existing == null) {
|
||||
existing = new TreeNode()
|
||||
existing.file = element
|
||||
existing.isFile = element.isFile()
|
||||
existing.parent = current
|
||||
fileToNode.put(element, existing)
|
||||
current.children.add(existing)
|
||||
@ -64,7 +65,7 @@ class FileTree<T> {
|
||||
private void doTraverse(TreeNode<T> node, FileTreeCallback<T> callback) {
|
||||
boolean leave = false
|
||||
if (node.file != null) {
|
||||
if (node.file.isFile())
|
||||
if (node.isFile)
|
||||
callback.onFile(node.file, node.value)
|
||||
else {
|
||||
leave = true
|
||||
@ -88,7 +89,7 @@ class FileTree<T> {
|
||||
node = fileToNode.get(parent)
|
||||
|
||||
node.children.each {
|
||||
if (it.file.isFile())
|
||||
if (it.isFile)
|
||||
callback.onFile(it.file, it.value)
|
||||
else
|
||||
callback.onDirectory(it.file)
|
||||
@ -98,6 +99,7 @@ class FileTree<T> {
|
||||
public static class TreeNode<T> {
|
||||
TreeNode parent
|
||||
File file
|
||||
boolean isFile
|
||||
T value;
|
||||
final Set<TreeNode> children = new HashSet<>()
|
||||
|
||||
|
@ -65,7 +65,8 @@ class HasherService {
|
||||
} else {
|
||||
eventBus.publish new FileHashingEvent(hashingFile: f)
|
||||
def hash = hasher.hashFile f
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
|
||||
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash.getRoot(), FileHasher.getPieceSize(f.length())),
|
||||
infoHash : hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
/**
|
||||
* Should be triggered by the old PersisterService
|
||||
* once it has finished reading the old file
|
||||
*
|
||||
* @see PersisterService
|
||||
*/
|
||||
class PersisterDoneEvent extends Event{
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.*
|
||||
import com.muwire.core.filefeeds.UIFilePublishedEvent
|
||||
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* A persister that stores information about the files shared using
|
||||
* individual JSON files in directories.
|
||||
*
|
||||
* The absolute path's 32bit hash to the shared file is used
|
||||
* to build the directory and filename.
|
||||
*
|
||||
* This persister only starts working once the old persister has finished loading
|
||||
* @see PersisterFolderService#getJsonPath
|
||||
*/
|
||||
@Log
|
||||
class PersisterFolderService extends BasePersisterService {
|
||||
|
||||
final static int CUT_LENGTH = 6
|
||||
|
||||
private final Core core;
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||
new Thread(r, "file persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
PersisterFolderService(Core core, File location, EventBus listener) {
|
||||
this.core = core;
|
||||
this.location = location
|
||||
this.listener = listener
|
||||
this.interval = interval
|
||||
timer = new Timer("file-folder persister timer", true)
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timer.cancel()
|
||||
persisterExecutor.shutdown()
|
||||
}
|
||||
|
||||
void onPersisterDoneEvent(PersisterDoneEvent persisterDoneEvent) {
|
||||
log.info("Old persister done")
|
||||
load()
|
||||
}
|
||||
|
||||
void onFileHashedEvent(FileHashedEvent hashedEvent) {
|
||||
if (core.getMuOptions().getAutoPublishSharedFiles() && hashedEvent.sharedFile != null)
|
||||
hashedEvent.sharedFile.publish(System.currentTimeMillis())
|
||||
persistFile(hashedEvent.sharedFile, hashedEvent.infoHash)
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent downloadedEvent) {
|
||||
if (core.getMuOptions().getShareDownloadedFiles()) {
|
||||
if (core.getMuOptions().getAutoPublishSharedFiles())
|
||||
downloadedEvent.downloadedFile.publish(System.currentTimeMillis())
|
||||
persistFile(downloadedEvent.downloadedFile, downloadedEvent.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rid of the json and hashlists of unshared files
|
||||
* @param unsharedEvent
|
||||
*/
|
||||
void onFileUnsharedEvent(FileUnsharedEvent unsharedEvent) {
|
||||
def jsonPath = getJsonPath(unsharedEvent.unsharedFile)
|
||||
def jsonFile = jsonPath.toFile()
|
||||
if(jsonFile.isFile()){
|
||||
jsonFile.delete()
|
||||
}
|
||||
def hashListPath = getHashListPath(unsharedEvent.unsharedFile)
|
||||
def hashListFile = hashListPath.toFile()
|
||||
if (hashListFile.isFile())
|
||||
hashListFile.delete()
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent loadedEvent) {
|
||||
if(loadedEvent.source == "PersisterService"){
|
||||
log.info("Migrating persisted file from PersisterService: "
|
||||
+ loadedEvent.loadedFile.file.absolutePath.toString())
|
||||
persistFile(loadedEvent.loadedFile, loadedEvent.infoHash)
|
||||
}
|
||||
}
|
||||
|
||||
void onUICommentEvent(UICommentEvent e) {
|
||||
persistFile(e.sharedFile,null)
|
||||
}
|
||||
|
||||
void onUIFilePublishedEvent(UIFilePublishedEvent e) {
|
||||
persistFile(e.sf, null)
|
||||
}
|
||||
|
||||
void onUIFileUnpublishedEvent(UIFileUnpublishedEvent e) {
|
||||
persistFile(e.sf, null)
|
||||
}
|
||||
|
||||
void load() {
|
||||
log.fine("Loading...")
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
|
||||
if (location.exists() && location.isDirectory()) {
|
||||
try {
|
||||
_load()
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files", e)
|
||||
}
|
||||
} else {
|
||||
location.mkdirs()
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads every JSON into memory
|
||||
*/
|
||||
private void _load() {
|
||||
int loaded = 0
|
||||
def slurper = new JsonSlurper()
|
||||
Files.walk(location.toPath())
|
||||
.filter({
|
||||
it.getFileName().toString().endsWith(".json")
|
||||
})
|
||||
.forEach({
|
||||
def parsed = slurper.parse it.toFile()
|
||||
def event = fromJsonLite parsed
|
||||
if (event == null) return
|
||||
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
Thread.sleep(20)
|
||||
|
||||
})
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
|
||||
private void persistFile(SharedFile sf, InfoHash ih) {
|
||||
persisterExecutor.submit({
|
||||
def jsonPath = getJsonPath(sf)
|
||||
|
||||
def startTime = System.currentTimeMillis()
|
||||
jsonPath.parent.toFile().mkdirs()
|
||||
jsonPath.toFile().withPrintWriter { writer ->
|
||||
def json = toJson sf
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
|
||||
if (ih != null) {
|
||||
def hashListPath = getHashListPath(sf)
|
||||
hashListPath.toFile().bytes = ih.hashList
|
||||
}
|
||||
log.fine("Time(ms) to write json+hashList: " + (System.currentTimeMillis() - startTime))
|
||||
} as Runnable)
|
||||
}
|
||||
private Path getJsonPath(SharedFile sf){
|
||||
def pathHash = sf.getB64PathHash()
|
||||
return Paths.get(
|
||||
location.getAbsolutePath(),
|
||||
pathHash.substring(0, CUT_LENGTH),
|
||||
pathHash.substring(CUT_LENGTH) + ".json"
|
||||
)
|
||||
}
|
||||
|
||||
private Path getHashListPath(SharedFile sf) {
|
||||
def pathHash = sf.getB64PathHash()
|
||||
return Paths.get(
|
||||
location.getAbsolutePath(),
|
||||
pathHash.substring(0, CUT_LENGTH),
|
||||
pathHash.substring(CUT_LENGTH) + ".hashlist"
|
||||
)
|
||||
}
|
||||
|
||||
InfoHash loadInfoHash(SharedFile sf) {
|
||||
def path = getHashListPath(sf)
|
||||
InfoHash.fromHashList(path.toFile().bytes)
|
||||
}
|
||||
}
|
@ -1,40 +1,24 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.Service
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
class PersisterService extends Service {
|
||||
class PersisterService extends BasePersisterService {
|
||||
|
||||
final File location
|
||||
final EventBus listener
|
||||
final int interval
|
||||
final Timer timer
|
||||
final FileManager fileManager
|
||||
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
|
||||
new Thread(r, "file persister")
|
||||
} as ThreadFactory)
|
||||
|
||||
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
|
||||
this.location = location
|
||||
@ -51,10 +35,6 @@ class PersisterService extends Service {
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
timer.schedule({load()} as TimerTask, 1)
|
||||
}
|
||||
|
||||
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
|
||||
persistFiles()
|
||||
}
|
||||
|
||||
void load() {
|
||||
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
|
||||
@ -69,6 +49,7 @@ class PersisterService extends Service {
|
||||
def event = fromJson parsed
|
||||
if (event != null) {
|
||||
log.fine("loaded file $event.loadedFile.file")
|
||||
event.source = "PersisterService"
|
||||
listener.publish event
|
||||
loaded++
|
||||
if (loaded % 10 == 0)
|
||||
@ -76,126 +57,18 @@ class PersisterService extends Service {
|
||||
}
|
||||
}
|
||||
}
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
// Backup the old hashes
|
||||
location.renameTo(
|
||||
new File(location.absolutePath + ".bak")
|
||||
)
|
||||
listener.publish(new PersisterDoneEvent())
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
} else {
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
listener.publish(new PersisterDoneEvent())
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
||||
private static FileLoadedEvent fromJson(def json) {
|
||||
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!(json.hashList instanceof List))
|
||||
throw new IllegalArgumentException()
|
||||
|
||||
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
file = file.getCanonicalFile()
|
||||
if (!file.exists() || file.isDirectory())
|
||||
return null
|
||||
long length = Long.valueOf(json.length)
|
||||
if (length != file.length())
|
||||
return null
|
||||
|
||||
List hashList = (List) json.hashList
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
||||
hashList.each {
|
||||
byte [] hash = Base64.decode it.toString()
|
||||
if (hash == null)
|
||||
throw new IllegalArgumentException()
|
||||
baos.write hash
|
||||
}
|
||||
byte[] hashListBytes = baos.toByteArray()
|
||||
|
||||
InfoHash ih = InfoHash.fromHashList(hashListBytes)
|
||||
byte [] root = Base64.decode(json.infoHash.toString())
|
||||
if (root == null)
|
||||
throw new IllegalArgumentException()
|
||||
if (!Arrays.equals(root, ih.getRoot()))
|
||||
return null
|
||||
|
||||
int pieceSize = 0
|
||||
if (json.pieceSize != null)
|
||||
pieceSize = json.pieceSize
|
||||
|
||||
if (json.sources != null) {
|
||||
List sources = (List)json.sources
|
||||
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
|
||||
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
|
||||
df.setComment(json.comment)
|
||||
return new FileLoadedEvent(loadedFile : df)
|
||||
}
|
||||
|
||||
|
||||
SharedFile sf = new SharedFile(file, ih, pieceSize)
|
||||
sf.setComment(json.comment)
|
||||
if (json.downloaders != null)
|
||||
sf.getDownloaders().addAll(json.downloaders)
|
||||
if (json.searchers != null) {
|
||||
json.searchers.each {
|
||||
Persona searcher = null
|
||||
if (it.searcher != null)
|
||||
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
|
||||
long timestamp = it.timestamp
|
||||
String query = it.query
|
||||
sf.hit(searcher, timestamp, query)
|
||||
}
|
||||
}
|
||||
return new FileLoadedEvent(loadedFile: sf)
|
||||
|
||||
}
|
||||
|
||||
private void persistFiles() {
|
||||
persisterExecutor.submit( {
|
||||
def sharedFiles = fileManager.getSharedFiles()
|
||||
|
||||
File tmp = File.createTempFile("muwire-files", "tmp")
|
||||
tmp.deleteOnExit()
|
||||
tmp.withPrintWriter { writer ->
|
||||
sharedFiles.each { k, v ->
|
||||
def json = toJson(k,v)
|
||||
json = JsonOutput.toJson(json)
|
||||
writer.println json
|
||||
}
|
||||
}
|
||||
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
tmp.delete()
|
||||
} as Runnable)
|
||||
}
|
||||
|
||||
private def toJson(File f, SharedFile sf) {
|
||||
def json = [:]
|
||||
json.file = sf.getB64EncodedFileName()
|
||||
json.length = sf.getCachedLength()
|
||||
InfoHash ih = sf.getInfoHash()
|
||||
json.infoHash = sf.getB64EncodedHashRoot()
|
||||
json.pieceSize = sf.getPieceSize()
|
||||
json.hashList = sf.getB64EncodedHashList()
|
||||
json.comment = sf.getComment()
|
||||
json.hits = sf.getHits()
|
||||
json.downloaders = sf.getDownloaders()
|
||||
|
||||
if (!sf.searches.isEmpty()) {
|
||||
Set searchers = new HashSet<>()
|
||||
sf.searches.each {
|
||||
def search = [:]
|
||||
if (it.searcher != null)
|
||||
search.searcher = it.searcher.toBase64()
|
||||
search.timestamp = it.timestamp
|
||||
search.query = it.query
|
||||
searchers.add(search)
|
||||
}
|
||||
json.searchers = searchers
|
||||
}
|
||||
|
||||
if (sf instanceof DownloadedFile) {
|
||||
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
|
||||
}
|
||||
|
||||
json
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIPersistFilesEvent extends Event {
|
||||
}
|
@ -77,18 +77,19 @@ class ResultsSender {
|
||||
if (it.getComment() != null) {
|
||||
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
|
||||
}
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def uiResultEvent = new UIResultEvent( sender : me,
|
||||
name : it.getFile().getName(),
|
||||
size : length,
|
||||
infohash : it.getInfoHash(),
|
||||
infohash : new InfoHash(it.getRoot()),
|
||||
pieceSize : pieceSize,
|
||||
uuid : uuid,
|
||||
browse : settings.browseFiles,
|
||||
sources : suggested,
|
||||
comment : comment,
|
||||
certificates : certificates,
|
||||
chat : chatServer.running.get() && settings.advertiseChat
|
||||
chat : chatServer.running.get() && settings.advertiseChat,
|
||||
feed : settings.fileFeed && settings.advertiseFeed
|
||||
)
|
||||
uiResultEvents << uiResultEvent
|
||||
}
|
||||
@ -119,7 +120,7 @@ class ResultsSender {
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
@ -138,10 +139,12 @@ class ResultsSender {
|
||||
os.write("Count: $results.length\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
boolean chat = chatServer.running.get() && 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))
|
||||
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
|
||||
results.each {
|
||||
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
|
||||
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
|
||||
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
|
||||
def json = jsonOutput.toJson(obj)
|
||||
dos.writeShort((short)json.length())
|
||||
@ -170,7 +173,7 @@ class ResultsSender {
|
||||
obj.type = "Result"
|
||||
obj.version = 2
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
|
||||
obj.infohash = Base64.encode(sf.getRoot())
|
||||
obj.size = sf.getCachedLength()
|
||||
obj.pieceSize = sf.getPieceSize()
|
||||
|
||||
|
@ -18,7 +18,8 @@ class UIResultEvent extends Event {
|
||||
boolean browse
|
||||
int certificates
|
||||
boolean chat
|
||||
|
||||
boolean feed
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||
|
@ -83,7 +83,7 @@ class UpdateClient {
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
if (e.downloadedFile.infoHash != updateInfoHash)
|
||||
if (e.infoHash != updateInfoHash)
|
||||
return
|
||||
updateDownloading = false
|
||||
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))
|
||||
|
@ -106,7 +106,7 @@ class ContentUploader extends Uploader {
|
||||
return done ? 100 : 0
|
||||
int position = mapped.position()
|
||||
int total = request.getRange().end - request.getRange().start
|
||||
(int)(position * 100.0 / total)
|
||||
(int)(position * 100.0d / total)
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,7 +45,7 @@ class HashListUploader extends Uploader {
|
||||
|
||||
@Override
|
||||
public synchronized int getProgress() {
|
||||
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||
(int)(mapped.position() * 100.0d / mapped.capacity())
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -12,6 +12,7 @@ import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.SourceDiscoveredEvent
|
||||
import com.muwire.core.files.FileManager
|
||||
import com.muwire.core.files.PersisterFolderService
|
||||
import com.muwire.core.mesh.Mesh
|
||||
import com.muwire.core.mesh.MeshManager
|
||||
|
||||
@ -22,6 +23,7 @@ import net.i2p.data.Base64
|
||||
public class UploadManager {
|
||||
private final EventBus eventBus
|
||||
private final FileManager fileManager
|
||||
private final PersisterFolderService persisterService
|
||||
private final MeshManager meshManager
|
||||
private final DownloadManager downloadManager
|
||||
private final MuWireSettings props
|
||||
@ -34,9 +36,11 @@ public class UploadManager {
|
||||
|
||||
public UploadManager(EventBus eventBus, FileManager fileManager,
|
||||
MeshManager meshManager, DownloadManager downloadManager,
|
||||
PersisterFolderService persisterService,
|
||||
MuWireSettings props) {
|
||||
this.eventBus = eventBus
|
||||
this.fileManager = fileManager
|
||||
this.persisterService = persisterService
|
||||
this.meshManager = meshManager
|
||||
this.downloadManager = downloadManager
|
||||
this.props = props
|
||||
@ -162,7 +166,7 @@ public class UploadManager {
|
||||
|
||||
InfoHash fullInfoHash
|
||||
if (downloader == null) {
|
||||
fullInfoHash = sharedFiles.iterator().next().infoHash
|
||||
fullInfoHash = persisterService.loadInfoHash(sharedFiles.iterator().next())
|
||||
} else {
|
||||
byte [] hashList = downloader.getInfoHash().getHashList()
|
||||
if (hashList != null && hashList.length > 0)
|
||||
|
@ -49,7 +49,7 @@ abstract class Uploader {
|
||||
final long now = System.currentTimeMillis()
|
||||
long interval = Math.max(1000, now - lastSpeedRead)
|
||||
lastSpeedRead = now;
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0 / interval)
|
||||
int currSpeed = (int) (dataSinceLastRead * 1000.0d / interval)
|
||||
dataSinceLastRead = 0
|
||||
|
||||
// normalize to speedArr.size
|
||||
|
@ -10,9 +10,9 @@ public class DownloadedFile extends SharedFile {
|
||||
|
||||
private final Set<Destination> sources;
|
||||
|
||||
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
|
||||
public DownloadedFile(File file, byte[] root, int pieceSize, Set<Destination> sources)
|
||||
throws IOException {
|
||||
super(file, infoHash, pieceSize);
|
||||
super(file, root, pieceSize);
|
||||
this.sources = sources;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,10 @@ package com.muwire.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -16,44 +19,49 @@ import net.i2p.data.Base64;
|
||||
public class SharedFile {
|
||||
|
||||
private final File file;
|
||||
private final InfoHash infoHash;
|
||||
private final byte[] root;
|
||||
private final int pieceSize;
|
||||
|
||||
private final String cachedPath;
|
||||
private final long cachedLength;
|
||||
|
||||
|
||||
private String b64PathHash;
|
||||
private final String b64EncodedFileName;
|
||||
private final String b64EncodedHashRoot;
|
||||
private final List<String> b64EncodedHashList;
|
||||
|
||||
private volatile String comment;
|
||||
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
|
||||
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
|
||||
private volatile boolean published;
|
||||
private volatile long publishedTimestamp;
|
||||
|
||||
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
|
||||
public SharedFile(File file, byte[] root, int pieceSize) throws IOException {
|
||||
this.file = file;
|
||||
this.infoHash = infoHash;
|
||||
this.root = root;
|
||||
this.pieceSize = pieceSize;
|
||||
this.cachedPath = file.getAbsolutePath();
|
||||
this.cachedLength = file.length();
|
||||
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
|
||||
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
|
||||
|
||||
List<String> b64List = new ArrayList<String>();
|
||||
byte[] tmp = new byte[32];
|
||||
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
|
||||
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
|
||||
b64List.add(Base64.encode(tmp));
|
||||
}
|
||||
this.b64EncodedHashList = b64List;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public InfoHash getInfoHash() {
|
||||
return infoHash;
|
||||
public byte[] getPathHash() throws NoSuchAlgorithmException {
|
||||
MessageDigest digester = MessageDigest.getInstance("SHA-256");
|
||||
digester.update(file.getAbsolutePath().getBytes());
|
||||
return digester.digest();
|
||||
}
|
||||
|
||||
public String getB64PathHash() throws NoSuchAlgorithmException {
|
||||
if(b64PathHash == null){
|
||||
b64PathHash = Base64.encode(getPathHash());
|
||||
}
|
||||
return b64PathHash;
|
||||
}
|
||||
|
||||
public byte[] getRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
@ -73,14 +81,6 @@ public class SharedFile {
|
||||
return b64EncodedFileName;
|
||||
}
|
||||
|
||||
public String getB64EncodedHashRoot() {
|
||||
return b64EncodedHashRoot;
|
||||
}
|
||||
|
||||
public List<String> getB64EncodedHashList() {
|
||||
return b64EncodedHashList;
|
||||
}
|
||||
|
||||
public String getCachedPath() {
|
||||
return cachedPath;
|
||||
}
|
||||
@ -116,10 +116,28 @@ public class SharedFile {
|
||||
public void addDownloader(String name) {
|
||||
downloaders.add(name);
|
||||
}
|
||||
|
||||
public void publish(long timestamp) {
|
||||
published = true;
|
||||
publishedTimestamp = timestamp;
|
||||
}
|
||||
|
||||
public void unpublish() {
|
||||
published = false;
|
||||
publishedTimestamp = 0;
|
||||
}
|
||||
|
||||
public boolean isPublished() {
|
||||
return published;
|
||||
}
|
||||
|
||||
public long getPublishedTimestamp() {
|
||||
return publishedTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return file.hashCode() ^ infoHash.hashCode();
|
||||
return file.hashCode() ^ Arrays.hashCode(root);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -127,7 +145,7 @@ public class SharedFile {
|
||||
if (!(o instanceof SharedFile))
|
||||
return false;
|
||||
SharedFile other = (SharedFile)o;
|
||||
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||
return file.equals(other.file) && Arrays.equals(root, other.root);
|
||||
}
|
||||
|
||||
public static class SearchEntry {
|
||||
@ -141,6 +159,18 @@ public class SharedFile {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Persona getSearcher() {
|
||||
return searcher;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return Objects.hash(searcher) ^ Objects.hash(timestamp) ^ query.hashCode();
|
||||
}
|
||||
|
81
core/src/main/java/com/muwire/core/filefeeds/Feed.java
Normal file
@ -0,0 +1,81 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
public class Feed {
|
||||
|
||||
private final Persona publisher;
|
||||
|
||||
private int updateInterval;
|
||||
private long lastUpdated;
|
||||
private volatile long lastUpdateAttempt;
|
||||
private int itemsToKeep;
|
||||
private boolean autoDownload;
|
||||
private boolean sequential;
|
||||
private FeedFetchStatus status;
|
||||
|
||||
public Feed(Persona publisher) {
|
||||
this.publisher = publisher;
|
||||
this.status = FeedFetchStatus.IDLE;
|
||||
}
|
||||
|
||||
public int getUpdateInterval() {
|
||||
return updateInterval;
|
||||
}
|
||||
|
||||
public void setUpdateInterval(int updateInterval) {
|
||||
this.updateInterval = updateInterval;
|
||||
}
|
||||
|
||||
public long getLastUpdated() {
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
public void setLastUpdated(long lastUpdated) {
|
||||
this.lastUpdated = lastUpdated;
|
||||
}
|
||||
|
||||
public int getItemsToKeep() {
|
||||
return itemsToKeep;
|
||||
}
|
||||
|
||||
public void setItemsToKeep(int itemsToKeep) {
|
||||
this.itemsToKeep = itemsToKeep;
|
||||
}
|
||||
|
||||
public boolean isAutoDownload() {
|
||||
return autoDownload;
|
||||
}
|
||||
|
||||
public void setAutoDownload(boolean autoDownload) {
|
||||
this.autoDownload = autoDownload;
|
||||
}
|
||||
|
||||
public Persona getPublisher() {
|
||||
return publisher;
|
||||
}
|
||||
|
||||
public void setStatus(FeedFetchStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public FeedFetchStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setSequential(boolean sequential) {
|
||||
this.sequential = sequential;
|
||||
}
|
||||
|
||||
public boolean isSequential() {
|
||||
return sequential;
|
||||
}
|
||||
|
||||
public void setLastUpdateAttempt(long lastUpdateAttempt) {
|
||||
this.lastUpdateAttempt = lastUpdateAttempt;
|
||||
}
|
||||
|
||||
public long getLastUpdateAttempt() {
|
||||
return lastUpdateAttempt;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
public enum FeedFetchStatus {
|
||||
IDLE(false),
|
||||
CONNECTING(true),
|
||||
FETCHING(true),
|
||||
FINISHED(false),
|
||||
FAILED(false);
|
||||
|
||||
private final boolean active;
|
||||
|
||||
FeedFetchStatus(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
}
|
79
core/src/main/java/com/muwire/core/filefeeds/FeedItem.java
Normal file
@ -0,0 +1,79 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.muwire.core.InfoHash;
|
||||
import com.muwire.core.Persona;
|
||||
|
||||
public class FeedItem {
|
||||
|
||||
private final Persona publisher;
|
||||
private final long timestamp;
|
||||
private final String name;
|
||||
private final long size;
|
||||
private final int pieceSize;
|
||||
private final InfoHash infoHash;
|
||||
private final int certificates;
|
||||
private final String comment;
|
||||
|
||||
public FeedItem(Persona publisher, long timestamp, String name, long size, int pieceSize, InfoHash infoHash,
|
||||
int certificates, String comment) {
|
||||
super();
|
||||
this.publisher = publisher;
|
||||
this.timestamp = timestamp;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
this.pieceSize = pieceSize;
|
||||
this.infoHash = infoHash;
|
||||
this.certificates = certificates;
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public Persona getPublisher() {
|
||||
return publisher;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public int getPieceSize() {
|
||||
return pieceSize;
|
||||
}
|
||||
|
||||
public InfoHash getInfoHash() {
|
||||
return infoHash;
|
||||
}
|
||||
|
||||
public int getCertificates() {
|
||||
return certificates;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(publisher, timestamp, name, infoHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof FeedItem))
|
||||
return false;
|
||||
FeedItem other = (FeedItem)o;
|
||||
return Objects.equals(publisher, other.publisher) &&
|
||||
timestamp == other.timestamp &&
|
||||
Objects.equals(name, other.name) &&
|
||||
Objects.equals(infoHash, other.infoHash);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.muwire.core.filefeeds;
|
||||
|
||||
public class InvalidFeedItemException extends Exception {
|
||||
|
||||
public InvalidFeedItemException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(String message, Throwable cause, boolean enableSuppression,
|
||||
boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(String message) {
|
||||
super(message);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidFeedItemException(Throwable cause) {
|
||||
super(cause);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
group = com.muwire
|
||||
version = 0.6.8
|
||||
i2pVersion = 0.9.44
|
||||
version = 0.6.12
|
||||
i2pVersion = 0.9.45
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
@ -126,4 +126,9 @@ mvcGroups {
|
||||
view = 'com.muwire.gui.ChatMonitorView'
|
||||
controller = 'com.muwire.gui.ChatMonitorController'
|
||||
}
|
||||
'feed-configuration' {
|
||||
model = 'com.muwire.gui.FeedConfigurationModel'
|
||||
view = 'com.muwire.gui.FeedConfigurationView'
|
||||
controller = 'com.muwire.gui.FeedConfigurationController'
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,9 @@ class BrowseController {
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = result
|
||||
params['host'] = result.getSender()
|
||||
params['infoHash'] = result.getInfohash()
|
||||
params['name'] = result.getName()
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
|
||||
|
||||
@ArtifactProviderFor(GriffonController)
|
||||
class FeedConfigurationController {
|
||||
@MVCMember @Nonnull
|
||||
FeedConfigurationModel model
|
||||
@MVCMember @Nonnull
|
||||
FeedConfigurationView view
|
||||
|
||||
@ControllerAction
|
||||
void save() {
|
||||
|
||||
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.core.eventBus.publish(new UIFeedConfigurationEvent(feed : model.feed))
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void cancel() {
|
||||
view.dialog.setVisible(false)
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ class FetchCertificatesController {
|
||||
core.eventBus.with {
|
||||
register(CertificateFetchEvent.class, this)
|
||||
register(CertificateFetchedEvent.class, this)
|
||||
publish(new UIFetchCertificatesEvent(host : model.result.sender, infoHash : model.result.infohash))
|
||||
publish(new UIFetchCertificatesEvent(host : model.host, infoHash : model.infoHash))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,15 +3,11 @@ package com.muwire.gui
|
||||
import griffon.core.GriffonApplication
|
||||
import griffon.core.artifact.GriffonController
|
||||
import griffon.core.controller.ControllerAction
|
||||
import griffon.core.mvc.MVCGroup
|
||||
import griffon.core.mvc.MVCGroupConfiguration
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import groovy.json.StringEscapeUtils
|
||||
import net.i2p.crypto.DSAEngine
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Signature
|
||||
import net.i2p.data.SigningPrivateKey
|
||||
|
||||
import java.awt.Desktop
|
||||
import java.awt.Toolkit
|
||||
@ -30,15 +26,18 @@ import com.muwire.core.Persona
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.SplitPattern
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.download.UIDownloadPausedEvent
|
||||
import com.muwire.core.download.UIDownloadResumedEvent
|
||||
import com.muwire.core.filecert.UICreateCertificateEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.filefeeds.Feed
|
||||
import com.muwire.core.filefeeds.FeedItem
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.filefeeds.UIFeedDeletedEvent
|
||||
import com.muwire.core.filefeeds.UIFeedUpdateEvent
|
||||
import com.muwire.core.filefeeds.UIFilePublishedEvent
|
||||
import com.muwire.core.filefeeds.UIFileUnpublishedEvent
|
||||
import com.muwire.core.files.FileUnsharedEvent
|
||||
import com.muwire.core.files.UIPersistFilesEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.trust.RemoteTrustList
|
||||
@ -371,7 +370,6 @@ class MainFrameController {
|
||||
sf.each {
|
||||
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
|
||||
}
|
||||
core.eventBus.publish(new UIPersistFilesEvent())
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
@ -514,6 +512,105 @@ class MainFrameController {
|
||||
clipboard.setContents(selection, null)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void publish() {
|
||||
def selectedFiles = view.selectedSharedFiles()
|
||||
if (selectedFiles == null || selectedFiles.isEmpty())
|
||||
return
|
||||
|
||||
if (model.publishButtonText == "Unpublish") {
|
||||
selectedFiles.each {
|
||||
it.unpublish()
|
||||
model.core.eventBus.publish(new UIFileUnpublishedEvent(sf : it))
|
||||
}
|
||||
} else {
|
||||
long now = System.currentTimeMillis()
|
||||
selectedFiles.stream().filter({!it.isPublished()}).forEach({
|
||||
it.publish(now)
|
||||
model.core.eventBus.publish(new UIFilePublishedEvent(sf : it))
|
||||
})
|
||||
}
|
||||
view.refreshSharedFiles()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void updateFileFeed() {
|
||||
Feed feed = view.selectedFeed()
|
||||
if (feed == null)
|
||||
return
|
||||
model.core.eventBus.publish(new UIFeedUpdateEvent(host: feed.getPublisher()))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void unsubscribeFileFeed() {
|
||||
Feed feed = view.selectedFeed()
|
||||
if (feed == null)
|
||||
return
|
||||
model.core.eventBus.publish(new UIFeedDeletedEvent(host : feed.getPublisher()))
|
||||
runInsideUIAsync {
|
||||
model.feeds.remove(feed)
|
||||
model.feedItems.clear()
|
||||
view.refreshFeeds()
|
||||
}
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void configureFileFeed() {
|
||||
Feed feed = view.selectedFeed()
|
||||
if (feed == null)
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['core'] = core
|
||||
params['feed'] = feed
|
||||
mvcGroup.createMVCGroup("feed-configuration", params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void downloadFeedItem() {
|
||||
List<FeedItem> items = view.selectedFeedItems()
|
||||
if (items == null || items.isEmpty())
|
||||
return
|
||||
Feed f = model.core.getFeedManager().getFeed(items.get(0).getPublisher())
|
||||
items.each {
|
||||
if (!model.canDownload(it.getInfoHash()))
|
||||
return
|
||||
File target = new File(application.context.get("muwire-settings").downloadLocation, it.getName())
|
||||
model.core.eventBus.publish(new UIDownloadFeedItemEvent(item : it, target : target, sequential : f.isSequential()))
|
||||
}
|
||||
view.showDownloadsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewFeedItemComment() {
|
||||
List<FeedItem> items = view.selectedFeedItems()
|
||||
if (items == null || items.size() != 1)
|
||||
return
|
||||
FeedItem item = items.get(0)
|
||||
|
||||
String groupId = Base64.encode(item.getInfoHash().getRoot())
|
||||
Map<String, Object> params = new HashMap<>()
|
||||
params['text'] = DataUtil.readi18nString(Base64.decode(item.getComment()))
|
||||
params['name'] = item.getName()
|
||||
|
||||
mvcGroup.createMVCGroup("show-comment", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void viewFeedItemCertificates() {
|
||||
List<FeedItem> items = view.selectedFeedItems()
|
||||
if (items == null || items.size() != 1)
|
||||
return
|
||||
FeedItem item = items.get(0)
|
||||
|
||||
def params = [:]
|
||||
params['core'] = core
|
||||
params['host'] = item.getPublisher()
|
||||
params['infoHash'] = item.getInfoHash()
|
||||
params['name'] = item.getName()
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
|
||||
void startChat(Persona p) {
|
||||
if (!mvcGroup.getChildrenGroups().containsKey(p.getHumanReadableName())) {
|
||||
def params = [:]
|
||||
@ -534,4 +631,4 @@ class MainFrameController {
|
||||
core = e.getNewValue()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +122,38 @@ class OptionsController {
|
||||
model.outBw = text
|
||||
settings.outBw = Integer.valueOf(text)
|
||||
}
|
||||
|
||||
// feed saving
|
||||
|
||||
boolean fileFeed = view.fileFeedCheckbox.model.isSelected()
|
||||
model.fileFeed = fileFeed
|
||||
settings.fileFeed = fileFeed
|
||||
|
||||
boolean advertiseFeed = view.advertiseFeedCheckbox.model.isSelected()
|
||||
model.advertiseFeed = advertiseFeed
|
||||
settings.advertiseFeed = advertiseFeed
|
||||
|
||||
boolean autoPublishSharedFiles = view.autoPublishSharedFilesCheckbox.model.isSelected()
|
||||
model.autoPublishSharedFiles = autoPublishSharedFiles
|
||||
settings.autoPublishSharedFiles = autoPublishSharedFiles
|
||||
|
||||
boolean defaultFeedAutoDownload = view.defaultFeedAutoDownloadCheckbox.model.isSelected()
|
||||
model.defaultFeedAutoDownload = defaultFeedAutoDownload
|
||||
settings.defaultFeedAutoDownload = defaultFeedAutoDownload
|
||||
|
||||
boolean defaultFeedSequential = view.defaultFeedSequentialCheckbox.model.isSelected()
|
||||
model.defaultFeedSequential = defaultFeedSequential
|
||||
settings.defaultFeedSequential = defaultFeedSequential
|
||||
|
||||
String defaultFeedItemsToKeep = view.defaultFeedItemsToKeepField.text
|
||||
model.defaultFeedItemsToKeep = defaultFeedItemsToKeep
|
||||
settings.defaultFeedItemsToKeep = Integer.parseInt(defaultFeedItemsToKeep)
|
||||
|
||||
String defaultFeedUpdateInterval = view.defaultFeedUpdateIntervalField.text
|
||||
model.defaultFeedUpdateInterval = defaultFeedUpdateInterval
|
||||
settings.defaultFeedUpdateInterval = Integer.parseInt(defaultFeedUpdateInterval)
|
||||
|
||||
// trust saving
|
||||
|
||||
boolean onlyTrusted = view.allowUntrustedCheckbox.model.isSelected()
|
||||
model.onlyTrusted = onlyTrusted
|
||||
|
@ -12,6 +12,8 @@ import javax.swing.JOptionPane
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.filefeeds.Feed
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
import com.muwire.core.trust.TrustEvent
|
||||
import com.muwire.core.trust.TrustLevel
|
||||
@ -107,6 +109,22 @@ class SearchTabController {
|
||||
mvcGroup.createMVCGroup("browse", groupId, params)
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void subscribe() {
|
||||
def sender = view.selectedSender()
|
||||
if (sender == null)
|
||||
return
|
||||
|
||||
Feed feed = new Feed(sender)
|
||||
feed.setAutoDownload(core.muOptions.defaultFeedAutoDownload)
|
||||
feed.setSequential(core.muOptions.defaultFeedSequential)
|
||||
feed.setItemsToKeep(core.muOptions.defaultFeedItemsToKeep)
|
||||
feed.setUpdateInterval(core.muOptions.defaultFeedUpdateInterval * 60 * 1000)
|
||||
|
||||
core.eventBus.publish(new UIFeedConfigurationEvent(feed : feed, newFeed: true))
|
||||
mvcGroup.parentGroup.view.showFeedsWindow.call()
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
void chat() {
|
||||
def sender = view.selectedSender()
|
||||
@ -139,7 +157,9 @@ class SearchTabController {
|
||||
return
|
||||
|
||||
def params = [:]
|
||||
params['result'] = event
|
||||
params['host'] = event.getSender()
|
||||
params['infoHash'] = event.getInfohash()
|
||||
params['name'] = event.getName()
|
||||
params['core'] = core
|
||||
mvcGroup.createMVCGroup("fetch-certificates", params)
|
||||
}
|
||||
|
@ -82,23 +82,23 @@ class Ready extends AbstractLifecycleHandler {
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, metadata["application.version"])
|
||||
Runtime.getRuntime().addShutdownHook({
|
||||
core.shutdown()
|
||||
})
|
||||
core.startServices()
|
||||
application.context.put("muwire-settings", props)
|
||||
application.context.put("core",core)
|
||||
application.getPropertyChangeListeners("core").each {
|
||||
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
|
||||
}
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
} catch (Exception bad) {
|
||||
log.log(Level.SEVERE,"couldn't initialize core",bad)
|
||||
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",
|
||||
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
|
||||
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
|
||||
System.exit(0)
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook({
|
||||
core.shutdown()
|
||||
})
|
||||
core.startServices()
|
||||
application.context.put("muwire-settings", props)
|
||||
application.context.put("core",core)
|
||||
application.getPropertyChangeListeners("core").each {
|
||||
it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
|
||||
}
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
}
|
||||
|
||||
private String selectNickname() {
|
||||
|
@ -0,0 +1,26 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.filefeeds.Feed
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
import griffon.transform.Observable
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class FeedConfigurationModel {
|
||||
Core core
|
||||
Feed feed
|
||||
|
||||
@Observable boolean autoDownload
|
||||
@Observable boolean sequential
|
||||
@Observable int updateInterval
|
||||
@Observable int itemsToKeep
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
autoDownload = feed.isAutoDownload()
|
||||
sequential = feed.isSequential()
|
||||
updateInterval = feed.getUpdateInterval() / 60000
|
||||
itemsToKeep = feed.getItemsToKeep()
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.filecert.CertificateFetchStatus
|
||||
import com.muwire.core.filefeeds.FeedItem
|
||||
import com.muwire.core.search.UIResultEvent
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
@ -9,7 +12,9 @@ import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
class FetchCertificatesModel {
|
||||
UIResultEvent result
|
||||
Persona host
|
||||
InfoHash infoHash
|
||||
String name
|
||||
|
||||
@Observable CertificateFetchStatus status
|
||||
@Observable int totalCertificates
|
||||
|
@ -28,6 +28,12 @@ import com.muwire.core.content.ContentControlEvent
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.filecert.CertificateCreatedEvent
|
||||
import com.muwire.core.filefeeds.Feed
|
||||
import com.muwire.core.filefeeds.FeedFetchEvent
|
||||
import com.muwire.core.filefeeds.FeedItemFetchedEvent
|
||||
import com.muwire.core.filefeeds.FeedLoadedEvent
|
||||
import com.muwire.core.filefeeds.UIDownloadFeedItemEvent
|
||||
import com.muwire.core.filefeeds.UIFeedConfigurationEvent
|
||||
import com.muwire.core.files.AllFilesLoadedEvent
|
||||
import com.muwire.core.files.DirectoryUnsharedEvent
|
||||
import com.muwire.core.files.DirectoryWatchedEvent
|
||||
@ -61,6 +67,7 @@ import griffon.transform.FXObservable
|
||||
import griffon.transform.Observable
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
@ArtifactProviderFor(GriffonModel)
|
||||
@ -89,6 +96,8 @@ class MainFrameModel {
|
||||
def trusted = []
|
||||
def distrusted = []
|
||||
def subscriptions = []
|
||||
def feeds = []
|
||||
def feedItems = []
|
||||
|
||||
boolean sessionRestored
|
||||
|
||||
@ -103,6 +112,14 @@ class MainFrameModel {
|
||||
@Observable boolean previewButtonEnabled
|
||||
@Observable String resumeButtonText
|
||||
@Observable boolean addCommentButtonEnabled
|
||||
@Observable boolean publishButtonEnabled
|
||||
@Observable String publishButtonText
|
||||
@Observable boolean updateFileFeedButtonEnabled
|
||||
@Observable boolean unsubscribeFileFeedButtonEnabled
|
||||
@Observable boolean configureFileFeedButtonEnabled
|
||||
@Observable boolean downloadFeedItemButtonEnabled
|
||||
@Observable boolean viewFeedItemCommentButtonEnabled
|
||||
@Observable boolean viewFeedItemCertificatesButtonEnabled
|
||||
@Observable boolean subscribeButtonEnabled
|
||||
@Observable boolean markNeutralFromTrustedButtonEnabled
|
||||
@Observable boolean markDistrustedButtonEnabled
|
||||
@ -118,6 +135,7 @@ class MainFrameModel {
|
||||
@Observable boolean downloadsPaneButtonEnabled
|
||||
@Observable boolean uploadsPaneButtonEnabled
|
||||
@Observable boolean monitorPaneButtonEnabled
|
||||
@Observable boolean feedsPaneButtonEnabled
|
||||
@Observable boolean trustPaneButtonEnabled
|
||||
@Observable boolean chatPaneButtonEnabled
|
||||
|
||||
@ -125,7 +143,7 @@ class MainFrameModel {
|
||||
|
||||
@Observable Downloader downloader
|
||||
|
||||
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
|
||||
private final Set<InfoHash> downloadInfoHashes = new ConcurrentHashSet<>()
|
||||
|
||||
@Observable volatile Core core
|
||||
|
||||
@ -215,6 +233,10 @@ class MainFrameModel {
|
||||
core.eventBus.register(TrustSubscriptionUpdatedEvent.class, this)
|
||||
core.eventBus.register(SearchEvent.class, this)
|
||||
core.eventBus.register(CertificateCreatedEvent.class, this)
|
||||
core.eventBus.register(FeedLoadedEvent.class, this)
|
||||
core.eventBus.register(FeedFetchEvent.class, this)
|
||||
core.eventBus.register(FeedItemFetchedEvent.class, this)
|
||||
core.eventBus.register(UIFeedConfigurationEvent.class, this)
|
||||
|
||||
core.muOptions.watchedKeywords.each {
|
||||
core.eventBus.publish(new ContentControlEvent(term : it, regex: false, add: true))
|
||||
@ -253,11 +275,13 @@ class MainFrameModel {
|
||||
distrusted.addAll(core.trustService.bad.values())
|
||||
|
||||
resumeButtonText = "Retry"
|
||||
publishButtonText = "Publish"
|
||||
|
||||
searchesPaneButtonEnabled = false
|
||||
downloadsPaneButtonEnabled = true
|
||||
uploadsPaneButtonEnabled = true
|
||||
monitorPaneButtonEnabled = true
|
||||
feedsPaneButtonEnabled = true
|
||||
trustPaneButtonEnabled = true
|
||||
chatPaneButtonEnabled = true
|
||||
|
||||
@ -363,6 +387,8 @@ class MainFrameModel {
|
||||
}
|
||||
|
||||
void onFileLoadedEvent(FileLoadedEvent e) {
|
||||
if (e.source == "PersisterService")
|
||||
return
|
||||
runInsideUIAsync {
|
||||
shared << e.loadedFile
|
||||
loadedFiles = shared.size()
|
||||
@ -649,4 +675,41 @@ class MainFrameModel {
|
||||
int requests
|
||||
boolean finished
|
||||
}
|
||||
|
||||
void onFeedLoadedEvent(FeedLoadedEvent e) {
|
||||
runInsideUIAsync {
|
||||
feeds << e.feed
|
||||
view.refreshFeeds()
|
||||
}
|
||||
}
|
||||
|
||||
void onFeedFetchEvent(FeedFetchEvent e) {
|
||||
runInsideUIAsync {
|
||||
view.refreshFeeds()
|
||||
}
|
||||
}
|
||||
|
||||
void onUIFeedConfigurationEvent(UIFeedConfigurationEvent e) {
|
||||
if (!e.newFeed)
|
||||
return
|
||||
runInsideUIAsync {
|
||||
if (feeds.contains(e.feed))
|
||||
return
|
||||
feeds << e.feed
|
||||
view.refreshFeeds()
|
||||
}
|
||||
}
|
||||
|
||||
void onFeedItemFetchedEvent(FeedItemFetchedEvent e) {
|
||||
Feed feed = core.feedManager.getFeed(e.item.getPublisher())
|
||||
if (feed == null || !feed.isAutoDownload())
|
||||
return
|
||||
if (!canDownload(e.item.getInfoHash()))
|
||||
return
|
||||
if (core.fileManager.isShared(e.item.getInfoHash()))
|
||||
return
|
||||
|
||||
File target = new File(core.getMuOptions().getDownloadLocation(), e.item.getName())
|
||||
core.eventBus.publish(new UIDownloadFeedItemEvent(item : e.item, target : target, sequential : feed.isSequential()))
|
||||
}
|
||||
}
|
@ -50,6 +50,15 @@ class OptionsModel {
|
||||
@Observable String inBw
|
||||
@Observable String outBw
|
||||
|
||||
// feed options
|
||||
@Observable boolean fileFeed
|
||||
@Observable boolean advertiseFeed
|
||||
@Observable boolean autoPublishSharedFiles
|
||||
@Observable boolean defaultFeedAutoDownload
|
||||
@Observable String defaultFeedItemsToKeep
|
||||
@Observable boolean defaultFeedSequential
|
||||
@Observable String defaultFeedUpdateInterval
|
||||
|
||||
// trust options
|
||||
@Observable boolean onlyTrusted
|
||||
@Observable boolean searchExtraHop
|
||||
@ -105,6 +114,14 @@ class OptionsModel {
|
||||
inBw = String.valueOf(settings.inBw)
|
||||
outBw = String.valueOf(settings.outBw)
|
||||
}
|
||||
|
||||
fileFeed = settings.fileFeed
|
||||
advertiseFeed = settings.advertiseFeed
|
||||
autoPublishSharedFiles = settings.autoPublishSharedFiles
|
||||
defaultFeedAutoDownload = settings.defaultFeedAutoDownload
|
||||
defaultFeedItemsToKeep = String.valueOf(settings.defaultFeedItemsToKeep)
|
||||
defaultFeedSequential = settings.defaultFeedSequential
|
||||
defaultFeedUpdateInterval = String.valueOf(settings.defaultFeedUpdateInterval)
|
||||
|
||||
onlyTrusted = !settings.allowUntrusted()
|
||||
searchExtraHop = settings.searchExtraHop
|
||||
|
@ -25,6 +25,7 @@ class SearchTabModel {
|
||||
@Observable boolean viewCommentActionEnabled
|
||||
@Observable boolean viewCertificatesActionEnabled
|
||||
@Observable boolean chatActionEnabled
|
||||
@Observable boolean subscribeActionEnabled
|
||||
@Observable boolean groupedByFile
|
||||
|
||||
Core core
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.SharedFile
|
||||
|
||||
import griffon.core.artifact.GriffonModel
|
||||
@ -21,6 +22,6 @@ class SharedFileModel {
|
||||
public void mvcGroupInit(Map<String,String> args) {
|
||||
searchers.addAll(sf.getSearches())
|
||||
downloaders.addAll(sf.getDownloaders())
|
||||
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
|
||||
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(new InfoHash(sf.getRoot()),[]))
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import griffon.core.artifact.GriffonView
|
||||
import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.event.WindowAdapter
|
||||
import java.awt.event.WindowEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ArtifactProviderFor(GriffonView)
|
||||
class FeedConfigurationView {
|
||||
@MVCMember @Nonnull
|
||||
FactoryBuilderSupport builder
|
||||
@MVCMember @Nonnull
|
||||
FeedConfigurationModel model
|
||||
|
||||
def dialog
|
||||
def p
|
||||
def mainFrame
|
||||
|
||||
def autoDownloadCheckbox
|
||||
def sequentialCheckbox
|
||||
def itemsToKeepField
|
||||
def updateIntervalField
|
||||
|
||||
void initUI() {
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, "Feed Configuration", true)
|
||||
dialog.setResizable(false)
|
||||
|
||||
p = builder.panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label("Configuration for feed " + model.feed.getPublisher().getHumanReadableName())
|
||||
}
|
||||
panel (constraints : BorderLayout.CENTER) {
|
||||
gridBagLayout()
|
||||
label(text : "Automatically download files from feed", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
autoDownloadCheckbox = checkBox(selected : bind {model.autoDownload}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Download files from feed sequentially", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
sequentialCheckbox = checkBox(selected : bind {model.sequential}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Feed items to store on disk (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
itemsToKeepField = textField(text : bind {model.itemsToKeep}, constraints:gbc(gridx :1, gridy:2, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Feed refresh frequency in minutes", constraints : gbc(gridx: 0, gridy : 3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
updateIntervalField = textField(text : bind {model.updateInterval}, constraints:gbc(gridx :1, gridy:3, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Save", saveAction)
|
||||
button(text : "Cancel", cancelAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
dialog.getContentPane().add(p)
|
||||
dialog.pack()
|
||||
dialog.setLocationRelativeTo(mainFrame)
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
public void windowClosed(WindowEvent e) {
|
||||
mvcGroup.destroy()
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ class FetchCertificatesView {
|
||||
void initUI() {
|
||||
int rowHeight = application.context.get("row-height")
|
||||
mainFrame = application.windowManager.findWindow("main-frame")
|
||||
dialog = new JDialog(mainFrame, model.result.name, true)
|
||||
dialog = new JDialog(mainFrame, model.name, true)
|
||||
dialog.setResizable(true)
|
||||
|
||||
p = builder.panel {
|
||||
|
@ -35,9 +35,13 @@ import javax.swing.tree.TreePath
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.download.Downloader
|
||||
import com.muwire.core.filefeeds.Feed
|
||||
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 java.awt.BorderLayout
|
||||
@ -75,6 +79,8 @@ class MainFrameView {
|
||||
def lastSharedSortEvent
|
||||
def trustTablesSortEvents = [:]
|
||||
def expansionListener = new TreeExpansions()
|
||||
def lastFeedsSortEvent
|
||||
def lastFeedItemsSortEvent
|
||||
|
||||
|
||||
UISettings settings
|
||||
@ -150,6 +156,7 @@ class MainFrameView {
|
||||
button(text: "Uploads", enabled : bind{model.uploadsPaneButtonEnabled}, actionPerformed : showUploadsWindow)
|
||||
if (settings.showMonitor)
|
||||
button(text: "Monitor", enabled: bind{model.monitorPaneButtonEnabled},actionPerformed : showMonitorWindow)
|
||||
button(text: "Feeds", enabled: bind {model.feedsPaneButtonEnabled}, actionPerformed : showFeedsWindow)
|
||||
button(text: "Trust", enabled:bind{model.trustPaneButtonEnabled},actionPerformed : showTrustWindow)
|
||||
button(text: "Chat", enabled : bind{model.chatPaneButtonEnabled}, actionPerformed : showChatWindow)
|
||||
}
|
||||
@ -289,8 +296,9 @@ class MainFrameView {
|
||||
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
|
||||
closureColumn(header : "Certified", preferredWidth : 50, type : Boolean, read : {
|
||||
Core core = application.context.get("core")
|
||||
core.certificateManager.hasLocalCertificate(it.getInfoHash())
|
||||
core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
|
||||
})
|
||||
closureColumn(header : "Published", preferredWidth : 50, type : Boolean, read : {row -> row.isPublished()})
|
||||
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
|
||||
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
|
||||
}
|
||||
@ -315,9 +323,11 @@ class MainFrameView {
|
||||
radioButton(text : "Table", selected : false, buttonGroup : sharedViewType, actionPerformed : showSharedFilesTable)
|
||||
}
|
||||
panel {
|
||||
button(text : "Share", actionPerformed : shareFiles)
|
||||
button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction)
|
||||
button(text : "Certify", enabled : bind {model.addCommentButtonEnabled}, issueCertificateAction)
|
||||
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)
|
||||
}
|
||||
panel {
|
||||
panel {
|
||||
@ -424,6 +434,56 @@ class MainFrameView {
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : "feeds window") {
|
||||
gridLayout(rows : 2, cols : 1)
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label(text: "Subscriptions")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "feeds-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.feeds) {
|
||||
closureColumn(header : "Publisher", preferredWidth: 350, type : String, read : {it.getPublisher().getHumanReadableName()})
|
||||
closureColumn(header : "Files", preferredWidth: 10, type : Integer, read : {model.core.feedManager.getFeedItems(it.getPublisher()).size()})
|
||||
closureColumn(header : "Last Updated", type : Long, read : {it.getLastUpdated()})
|
||||
closureColumn(header : "Status", preferredWidth: 10, type : String, read : {it.getStatus().toString()})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel (constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Update", enabled : bind {model.updateFileFeedButtonEnabled}, updateFileFeedAction)
|
||||
button(text : "Unsubscribe", enabled : bind {model.unsubscribeFileFeedButtonEnabled}, unsubscribeFileFeedAction)
|
||||
button(text : "Configure", enabled : bind {model.configureFileFeedButtonEnabled}, configureFileFeedAction)
|
||||
}
|
||||
}
|
||||
panel {
|
||||
borderLayout()
|
||||
panel (constraints : BorderLayout.NORTH) {
|
||||
label(text : "Published Files")
|
||||
}
|
||||
scrollPane(constraints : BorderLayout.CENTER) {
|
||||
table(id : "feed-items-table", autoCreateRowSorter : true, rowHeight : rowHeight) {
|
||||
tableModel(list : model.feedItems) {
|
||||
closureColumn(header : "Name", preferredWidth: 350, type : String, read : {it.getName()})
|
||||
closureColumn(header : "Size", preferredWidth: 10, type : Long, read : {it.getSize()})
|
||||
closureColumn(header : "Comment", preferredWidth: 10, type : Boolean, read : {it.getComment() != null})
|
||||
closureColumn(header : "Certificates", preferredWidth: 10, type : Integer, read : {it.getCertificates()})
|
||||
closureColumn(header : "Downloaded", preferredWidth: 10, type : Boolean, read : {
|
||||
InfoHash ih = it.getInfoHash()
|
||||
model.core.fileManager.isShared(ih)
|
||||
})
|
||||
closureColumn(header: "Date", type : Long, read : {it.getTimestamp()})
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : BorderLayout.SOUTH) {
|
||||
button(text : "Download", enabled : bind {model.downloadFeedItemButtonEnabled}, downloadFeedItemAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewFeedItemCommentButtonEnabled}, viewFeedItemCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewFeedItemCertificatesButtonEnabled}, viewFeedItemCertificatesAction )
|
||||
}
|
||||
}
|
||||
}
|
||||
panel(constraints : "trust window") {
|
||||
gridLayout(rows : 2, cols : 1)
|
||||
panel {
|
||||
@ -681,14 +741,30 @@ class MainFrameView {
|
||||
if (selectedFiles == null || selectedFiles.isEmpty())
|
||||
return
|
||||
model.addCommentButtonEnabled = true
|
||||
model.publishButtonEnabled = true
|
||||
boolean unpublish = true
|
||||
selectedFiles.each {
|
||||
unpublish &= it.isPublished()
|
||||
}
|
||||
model.publishButtonText = unpublish ? "Unpublish" : "Publish"
|
||||
})
|
||||
|
||||
def sharedFilesTree = builder.getVariable("shared-files-tree")
|
||||
sharedFilesTree.addMouseListener(sharedFilesMouseListener)
|
||||
|
||||
sharedFilesTree.addTreeSelectionListener({
|
||||
def selectedNode = sharedFilesTree.getLastSelectedPathComponent()
|
||||
model.addCommentButtonEnabled = selectedNode != null
|
||||
|
||||
model.publishButtonEnabled = selectedNode != null
|
||||
|
||||
def selectedFiles = selectedSharedFiles()
|
||||
if (selectedFiles == null || selectedFiles.isEmpty())
|
||||
return
|
||||
boolean unpublish = true
|
||||
selectedFiles.each {
|
||||
unpublish &= it.isPublished()
|
||||
}
|
||||
model.publishButtonText = unpublish ? "Unpublish" : "Publish"
|
||||
})
|
||||
|
||||
sharedFilesTree.addTreeExpansionListener(expansionListener)
|
||||
@ -744,6 +820,98 @@ class MainFrameView {
|
||||
}
|
||||
})
|
||||
|
||||
// feeds table
|
||||
def feedsTable = builder.getVariable("feeds-table")
|
||||
feedsTable.rowSorter.addRowSorterListener({evt -> lastFeedsSortEvent = evt})
|
||||
feedsTable.rowSorter.setSortsOnUpdates(true)
|
||||
feedsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
feedsTable.setDefaultRenderer(Long.class, new DateRenderer())
|
||||
selectionModel = feedsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
Feed selectedFeed = selectedFeed()
|
||||
if (selectedFeed == null) {
|
||||
model.updateFileFeedButtonEnabled = false
|
||||
model.unsubscribeFileFeedButtonEnabled = false
|
||||
model.configureFileFeedButtonEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
model.unsubscribeFileFeedButtonEnabled = true
|
||||
model.configureFileFeedButtonEnabled = true
|
||||
model.updateFileFeedButtonEnabled = !selectedFeed.getStatus().isActive()
|
||||
|
||||
def items = model.core.feedManager.getFeedItems(selectedFeed.getPublisher())
|
||||
model.feedItems.clear()
|
||||
model.feedItems.addAll(items)
|
||||
|
||||
def feedItemsTable = builder.getVariable("feed-items-table")
|
||||
int selectedItemRow = feedItemsTable.getSelectedRow()
|
||||
feedItemsTable.model.fireTableDataChanged()
|
||||
if (selectedItemRow >= 0 && selectedItemRow < items.size())
|
||||
feedItemsTable.selectionModel.setSelectionInterval(selectedItemRow, selectedItemRow)
|
||||
})
|
||||
feedsTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if(e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
|
||||
showFeedsPopupMenu(e)
|
||||
}
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if(e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
|
||||
showFeedsPopupMenu(e)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// feed items table
|
||||
def feedItemsTable = builder.getVariable("feed-items-table")
|
||||
feedItemsTable.rowSorter.addRowSorterListener({evt -> lastFeedItemsSortEvent = evt})
|
||||
feedItemsTable.rowSorter.setSortsOnUpdates(true)
|
||||
feedItemsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
feedItemsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
feedItemsTable.columnModel.getColumn(5).setCellRenderer(new DateRenderer())
|
||||
|
||||
selectionModel = feedItemsTable.getSelectionModel()
|
||||
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
List<FeedItem> selectedItems = selectedFeedItems()
|
||||
if (selectedItems == null || selectedItems.isEmpty()) {
|
||||
model.downloadFeedItemButtonEnabled = false
|
||||
model.viewFeedItemCommentButtonEnabled = false
|
||||
model.viewFeedItemCertificatesButtonEnabled = false
|
||||
return
|
||||
}
|
||||
model.downloadFeedItemButtonEnabled = true
|
||||
model.viewFeedItemCommentButtonEnabled = false
|
||||
model.viewFeedItemCertificatesButtonEnabled = false
|
||||
if (selectedItems.size() == 1) {
|
||||
FeedItem item = selectedItems.get(0)
|
||||
model.viewFeedItemCommentButtonEnabled = item.getComment() != null
|
||||
model.viewFeedItemCertificatesButtonEnabled = item.getCertificates() > 0
|
||||
}
|
||||
})
|
||||
feedItemsTable.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
List<FeedItem> selectedItems = selectedFeedItems()
|
||||
if (e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
|
||||
showFeedItemsPopupMenu(e)
|
||||
else if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 &&
|
||||
selectedItems != null && selectedItems.size() == 1 &&
|
||||
model.canDownload(selectedItems.get(0).getInfoHash())) {
|
||||
mvcGroup.controller.downloadFeedItem()
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (e.isPopupTrigger() || e.getButton() == MouseEvent.BUTTON3)
|
||||
showFeedItemsPopupMenu(e)
|
||||
}
|
||||
})
|
||||
|
||||
// subscription table
|
||||
def subscriptionTable = builder.getVariable("subscription-table")
|
||||
subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
@ -911,7 +1079,7 @@ class MainFrameView {
|
||||
String roots = ""
|
||||
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
|
||||
SharedFile selected = iterator.next()
|
||||
String root = Base64.encode(selected.infoHash.getRoot())
|
||||
String root = Base64.encode(selected.getRoot())
|
||||
roots += root
|
||||
if (iterator.hasNext())
|
||||
roots += "\n"
|
||||
@ -1006,6 +1174,52 @@ class MainFrameView {
|
||||
showPopupMenu(menu, e)
|
||||
}
|
||||
|
||||
void showFeedsPopupMenu(MouseEvent e) {
|
||||
Feed feed = selectedFeed()
|
||||
if (feed == null)
|
||||
return
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
if (model.updateFileFeedButtonEnabled) {
|
||||
JMenuItem update = new JMenuItem("Update")
|
||||
update.addActionListener({mvcGroup.controller.updateFileFeed()})
|
||||
menu.add(update)
|
||||
}
|
||||
|
||||
JMenuItem unsubscribe = new JMenuItem("Unsubscribe")
|
||||
unsubscribe.addActionListener({mvcGroup.controller.unsubscribeFileFeed()})
|
||||
menu.add(unsubscribe)
|
||||
|
||||
JMenuItem configure = new JMenuItem("Configure")
|
||||
configure.addActionListener({mvcGroup.controller.configureFileFeed()})
|
||||
menu.add(configure)
|
||||
|
||||
showPopupMenu(menu,e)
|
||||
}
|
||||
|
||||
void showFeedItemsPopupMenu(MouseEvent e) {
|
||||
List<FeedItem> items = selectedFeedItems()
|
||||
if (items == null || items.isEmpty())
|
||||
return
|
||||
// TODO: finish
|
||||
JPopupMenu menu = new JPopupMenu()
|
||||
if (model.downloadFeedItemButtonEnabled) {
|
||||
JMenuItem download = new JMenuItem("Download")
|
||||
download.addActionListener({mvcGroup.controller.downloadFeedItem()})
|
||||
menu.add(download)
|
||||
}
|
||||
if (model.viewFeedItemCommentButtonEnabled) {
|
||||
JMenuItem viewComment = new JMenuItem("View Comment")
|
||||
viewComment.addActionListener({mvcGroup.controller.viewFeedItemComment()})
|
||||
menu.add(viewComment)
|
||||
}
|
||||
if (model.viewFeedItemCertificatesButtonEnabled) {
|
||||
JMenuItem viewCertificates = new JMenuItem("View Certificates")
|
||||
viewCertificates.addActionListener({mvcGroup.controller.viewFeedItemCertificates()})
|
||||
menu.add(viewCertificates)
|
||||
}
|
||||
showPopupMenu(menu, e)
|
||||
}
|
||||
|
||||
def selectedUploader() {
|
||||
def uploadsTable = builder.getVariable("uploads-table")
|
||||
int selectedRow = uploadsTable.getSelectedRow()
|
||||
@ -1056,6 +1270,7 @@ class MainFrameView {
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.feedsPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
@ -1068,6 +1283,7 @@ class MainFrameView {
|
||||
model.downloadsPaneButtonEnabled = false
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.feedsPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
@ -1080,6 +1296,7 @@ class MainFrameView {
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = false
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.feedsPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
@ -1092,6 +1309,20 @@ class MainFrameView {
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = false
|
||||
model.feedsPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
}
|
||||
|
||||
def showFeedsWindow = {
|
||||
def cardsPanel = builder.getVariable("cards-panel")
|
||||
cardsPanel.getLayout().show(cardsPanel,"feeds window")
|
||||
model.searchesPaneButtonEnabled = true
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.feedsPaneButtonEnabled = false
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
@ -1104,6 +1335,7 @@ class MainFrameView {
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.feedsPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = false
|
||||
model.chatPaneButtonEnabled = true
|
||||
chatNotificator.mainWindowDeactivated()
|
||||
@ -1116,6 +1348,7 @@ class MainFrameView {
|
||||
model.downloadsPaneButtonEnabled = true
|
||||
model.uploadsPaneButtonEnabled = true
|
||||
model.monitorPaneButtonEnabled = true
|
||||
model.feedsPaneButtonEnabled = true
|
||||
model.trustPaneButtonEnabled = true
|
||||
model.chatPaneButtonEnabled = false
|
||||
chatNotificator.mainWindowActivated()
|
||||
@ -1172,6 +1405,43 @@ class MainFrameView {
|
||||
builder.getVariable("shared-files-table").model.fireTableDataChanged()
|
||||
}
|
||||
|
||||
public void refreshFeeds() {
|
||||
JTable feedsTable = builder.getVariable("feeds-table")
|
||||
int selectedFeed = feedsTable.getSelectedRow()
|
||||
feedsTable.model.fireTableDataChanged()
|
||||
if (selectedFeed >= 0)
|
||||
feedsTable.selectionModel.setSelectionInterval(selectedFeed, selectedFeed)
|
||||
|
||||
JTable feedItemsTable = builder.getVariable("feed-items-table")
|
||||
feedItemsTable.model.fireTableDataChanged()
|
||||
}
|
||||
|
||||
Feed selectedFeed() {
|
||||
JTable feedsTable = builder.getVariable("feeds-table")
|
||||
int row = feedsTable.getSelectedRow()
|
||||
if (row < 0)
|
||||
return null
|
||||
if (lastFeedsSortEvent != null)
|
||||
row = feedsTable.rowSorter.convertRowIndexToModel(row)
|
||||
model.feeds[row]
|
||||
}
|
||||
|
||||
List<FeedItem> selectedFeedItems() {
|
||||
JTable feedItemsTable = builder.getVariable("feed-items-table")
|
||||
int [] selectedRows = feedItemsTable.getSelectedRows()
|
||||
if (selectedRows.length == 0)
|
||||
return null
|
||||
List<FeedItem> rv = new ArrayList<>()
|
||||
if (lastFeedItemsSortEvent != null) {
|
||||
for (int i = 0; i < selectedRows.length; i++) {
|
||||
selectedRows[i] = feedItemsTable.rowSorter.convertRowIndexToModel(selectedRows[i])
|
||||
}
|
||||
}
|
||||
for (int selectedRow : selectedRows)
|
||||
rv.add(model.feedItems[selectedRow])
|
||||
rv
|
||||
}
|
||||
|
||||
private void closeApplication() {
|
||||
Core core = application.getContext().get("core")
|
||||
|
||||
|
@ -32,6 +32,7 @@ class OptionsView {
|
||||
def i
|
||||
def u
|
||||
def bandwidth
|
||||
def feed
|
||||
def trust
|
||||
def chat
|
||||
|
||||
@ -66,6 +67,14 @@ class OptionsView {
|
||||
|
||||
def inBwField
|
||||
def outBwField
|
||||
|
||||
def fileFeedCheckbox
|
||||
def advertiseFeedCheckbox
|
||||
def autoPublishSharedFilesCheckbox
|
||||
def defaultFeedAutoDownloadCheckbox
|
||||
def defaultFeedItemsToKeepField
|
||||
def defaultFeedSequentialCheckbox
|
||||
def defaultFeedUpdateIntervalField
|
||||
|
||||
def allowUntrustedCheckbox
|
||||
def searchExtraHopCheckbox
|
||||
@ -257,6 +266,32 @@ class OptionsView {
|
||||
}
|
||||
panel(constraints : gbc(gridx: 0, gridy: 1, weighty: 100))
|
||||
}
|
||||
feed = builder.panel {
|
||||
gridBagLayout()
|
||||
panel (border : titledBorder(title : "General Feed Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
constraints : gbc(gridx : 0, gridy : 0, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
||||
gridBagLayout()
|
||||
label(text : "Enable file feed", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
fileFeedCheckbox = checkBox(selected : bind {model.fileFeed}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Advertise feed in search results", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
advertiseFeedCheckbox = checkBox(selected : bind {model.advertiseFeed}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Automatically publish shared files", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
autoPublishSharedFilesCheckbox = checkBox(selected : bind {model.autoPublishSharedFiles}, constraints : gbc(gridx: 1, gridy : 2, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel (border : titledBorder(title : "Default Settings For New Feeds", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
constraints : gbc(gridx : 0, gridy : 1, fill : GridBagConstraints.HORIZONTAL, weightx: 100)) {
|
||||
gridBagLayout()
|
||||
label(text : "Automatically download files from feed", constraints : gbc(gridx: 0, gridy : 0, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
defaultFeedAutoDownloadCheckbox = checkBox(selected : bind {model.defaultFeedAutoDownload}, constraints : gbc(gridx: 1, gridy : 0, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Download files from feed sequentially", constraints : gbc(gridx: 0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
defaultFeedSequentialCheckbox = checkBox(selected : bind {model.defaultFeedSequential}, constraints : gbc(gridx: 1, gridy : 1, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Feed items to store on disk (-1 means unlimited)", constraints : gbc(gridx: 0, gridy : 2, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
defaultFeedItemsToKeepField = textField(text : bind {model.defaultFeedItemsToKeep}, constraints:gbc(gridx :1, gridy:2, anchor : GridBagConstraints.LINE_END))
|
||||
label(text : "Feed refresh frequency in minutes", constraints : gbc(gridx: 0, gridy : 3, anchor : GridBagConstraints.LINE_START, weightx: 100))
|
||||
defaultFeedUpdateIntervalField = textField(text : bind {model.defaultFeedUpdateInterval}, constraints:gbc(gridx :1, gridy:3, anchor : GridBagConstraints.LINE_END))
|
||||
}
|
||||
panel(constraints : gbc(gridx: 0, gridy : 2, weighty: 100))
|
||||
}
|
||||
trust = builder.panel {
|
||||
gridBagLayout()
|
||||
panel (border : titledBorder(title : "Trust Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP),
|
||||
@ -311,6 +346,7 @@ class OptionsView {
|
||||
if (core.router != null) {
|
||||
tabbedPane.addTab("Bandwidth", bandwidth)
|
||||
}
|
||||
tabbedPane.addTab("Feed", feed)
|
||||
tabbedPane.addTab("Trust", trust)
|
||||
tabbedPane.addTab("Chat", chat)
|
||||
|
||||
|
@ -74,6 +74,7 @@ class SearchTabView {
|
||||
closureColumn(header : "Sender", preferredWidth : 500, type: String, read : {row -> row.getHumanReadableName()})
|
||||
closureColumn(header : "Results", preferredWidth : 20, type: Integer, read : {row -> model.sendersBucket[row].size()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type: Boolean, read : {row -> model.sendersBucket[row].first().browse})
|
||||
closureColumn(header : "Feed", preferredWidth : 20, type : Boolean, read : {row -> model.sendersBucket[row].first().feed})
|
||||
closureColumn(header : "Chat", preferredWidth : 20, type : Boolean, read : {row -> model.sendersBucket[row].first().chat})
|
||||
closureColumn(header : "Trust", preferredWidth : 50, type: String, read : { row ->
|
||||
model.core.trustService.getLevel(row.destination).toString()
|
||||
@ -85,6 +86,7 @@ class SearchTabView {
|
||||
gridLayout(rows: 1, cols : 2)
|
||||
panel (border : etchedBorder()){
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
button(text : "Subscribe", enabled : bind {model.subscribeActionEnabled}, subscribeAction)
|
||||
button(text : "Chat", enabled : bind{model.chatActionEnabled}, chatAction)
|
||||
}
|
||||
panel (border : etchedBorder()){
|
||||
@ -156,6 +158,14 @@ class SearchTabView {
|
||||
}
|
||||
count
|
||||
})
|
||||
closureColumn(header : "Feeds", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
if (it.feed)
|
||||
count++
|
||||
}
|
||||
count
|
||||
})
|
||||
closureColumn(header : "Chat Hosts", preferredWidth : 20, type : Integer, read : {
|
||||
int count = 0
|
||||
model.hashBucket[it].each {
|
||||
@ -187,6 +197,7 @@ class SearchTabView {
|
||||
tableModel(list : model.senders2) {
|
||||
closureColumn(header : "Sender", preferredWidth : 350, type : String, read : {it.sender.getHumanReadableName()})
|
||||
closureColumn(header : "Browse", preferredWidth : 20, type : Boolean, read : {it.browse})
|
||||
closureColumn(header : "Feed", preferredWidth : 20, type: Boolean, read : {it.feed})
|
||||
closureColumn(header : "Chat", preferredWidth : 20, type : Boolean, read : {it.chat})
|
||||
closureColumn(header : "Comment", preferredWidth : 20, type : Boolean, read : {it.comment != null})
|
||||
closureColumn(header : "Certificates", preferredWidth : 20, type: Integer, read : {it.certificates})
|
||||
@ -200,6 +211,7 @@ class SearchTabView {
|
||||
gridLayout(rows : 1, cols : 2)
|
||||
panel (border : etchedBorder()) {
|
||||
button(text : "Browse Host", enabled : bind {model.browseActionEnabled}, browseAction)
|
||||
button(text : "Subscribe", enabled : bind {model.subscribeActionEnabled}, subscribeAction)
|
||||
button(text : "Chat", enabled : bind{model.chatActionEnabled}, chatAction)
|
||||
button(text : "View Comment", enabled : bind {model.viewCommentActionEnabled}, showCommentAction)
|
||||
button(text : "View Certificates", enabled : bind {model.viewCertificatesActionEnabled}, viewCertificatesAction)
|
||||
@ -308,6 +320,7 @@ class SearchTabView {
|
||||
if (result == null) {
|
||||
model.viewCommentActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
model.subscribeActionEnabled = false
|
||||
return
|
||||
} else {
|
||||
model.viewCommentActionEnabled = result.comment != null
|
||||
@ -326,12 +339,14 @@ class SearchTabView {
|
||||
if (row < 0) {
|
||||
model.trustButtonsEnabled = false
|
||||
model.browseActionEnabled = false
|
||||
model.chatActionEnabled = false
|
||||
model.subscribeActionEnabled = false
|
||||
return
|
||||
} else {
|
||||
Persona sender = model.senders[row]
|
||||
model.browseActionEnabled = model.sendersBucket[sender].first().browse
|
||||
model.chatActionEnabled = model.sendersBucket[sender].first().chat
|
||||
model.subscribeActionEnabled = model.sendersBucket[sender].first().feed &&
|
||||
model.core.feedManager.getFeed(sender) == null
|
||||
model.trustButtonsEnabled = true
|
||||
model.results.clear()
|
||||
model.results.addAll(model.sendersBucket[sender])
|
||||
@ -386,16 +401,19 @@ class SearchTabView {
|
||||
if (row < 0 || model.senders2[row] == null) {
|
||||
model.browseActionEnabled = false
|
||||
model.chatActionEnabled = false
|
||||
model.subscribeActionEnabled = false
|
||||
model.viewCertificatesActionEnabled = false
|
||||
model.trustButtonsEnabled = false
|
||||
model.viewCommentActionEnabled = false
|
||||
return
|
||||
}
|
||||
model.browseActionEnabled = model.senders2[row].browse
|
||||
model.chatActionEnabled = model.senders2[row].chat
|
||||
UIResultEvent e = model.senders2[row]
|
||||
model.browseActionEnabled = e.browse
|
||||
model.chatActionEnabled = e.chat
|
||||
model.subscribeActionEnabled = e.feed && model.core.feedManager.getFeed(e.getSender()) == null
|
||||
model.trustButtonsEnabled = true
|
||||
model.viewCommentActionEnabled = model.senders2[row].comment != null
|
||||
model.viewCertificatesActionEnabled = model.senders2[row].certificates > 0
|
||||
model.viewCommentActionEnabled = e.comment != null
|
||||
model.viewCertificatesActionEnabled = e.certificates > 0
|
||||
})
|
||||
|
||||
if (settings.groupByFile)
|
||||
|
@ -6,8 +6,8 @@ class DownloaderComparator implements Comparator<Downloader>{
|
||||
|
||||
@Override
|
||||
public int compare(Downloader o1, Downloader o2) {
|
||||
double d1 = o1.donePieces() * 1.0 / o1.nPieces
|
||||
double d2 = o2.donePieces() * 1.0 / o2.nPieces
|
||||
double d1 = o1.donePieces().toDouble() / o1.nPieces
|
||||
double d2 = o2.donePieces().toDouble() / o2.nPieces
|
||||
return Double.compare(d1, d2);
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ task generateWebXML {
|
||||
def jasper = new File("$buildDir/tmp_jsp/web.xml.jasper")
|
||||
templateText = templateText.replaceAll("__JASPER__", jasper.text)
|
||||
templateText = templateText.replaceAll("__VERSION__", project.version)
|
||||
templateText = templateText.replaceAll("__BUILD_NUMBER__", project.buildNumber)
|
||||
def webXml = new File("$buildDir/tmp_jsp/web.xml")
|
||||
webXml.text = templateText
|
||||
}
|
||||
|
@ -323,28 +323,40 @@ See also .menu-icon
|
||||
font-weight: 300;
|
||||
margin: 0 24px 0 48px;
|
||||
}
|
||||
.menuitem.identities .menu-icon:before {
|
||||
content: url("images/identities.png");
|
||||
}
|
||||
.menuitem.address-book .menu-icon:before {
|
||||
content: url("images/addressbook.png");
|
||||
}
|
||||
|
||||
.menuitem.settings .menu-icon:before {
|
||||
content: url("images/settings.png");
|
||||
content: url("images/ConfigurationPage.png");
|
||||
}
|
||||
.menuitem.downloads .menu-icon:before {
|
||||
content: url("images/inbox.png");
|
||||
content: url("images/Downloads.png");
|
||||
}
|
||||
.menuitem.uploads .menu-icon:before {
|
||||
content: url("images/uploads.png");
|
||||
}
|
||||
.menuitem.search .menu-icon:before {
|
||||
content: url("images/delay.png");
|
||||
content: url("images/Search.png");
|
||||
}
|
||||
.menuitem.shared .menu-icon:before {
|
||||
content: url("images/folder.png");
|
||||
content: url("images/SharedFiles.png");
|
||||
}
|
||||
.menuitem.browse .menu-icon:before {
|
||||
content : url("images/BrowseHost.png")
|
||||
}
|
||||
.menuitem.feeds .menu-icon:before {
|
||||
content : url("images/Feeds.png")
|
||||
}
|
||||
.menuitem.trustUsers .menu-icon:before {
|
||||
content : url("images/TrustUsers.png")
|
||||
}
|
||||
.menuitem.trustList .menu-icon:before {
|
||||
content : url("images/TrustList.png")
|
||||
}
|
||||
.menuitem.aboutMe .menu-icon:before {
|
||||
content : url("images/AboutMe.png")
|
||||
}
|
||||
.menuitem.muStatus .menu-icon:before {
|
||||
content : url("images/MuStatus.png")
|
||||
}
|
||||
|
||||
|
||||
/* Main content */
|
||||
|
||||
@ -463,7 +475,7 @@ table td, th {
|
||||
padding-top: 3px;
|
||||
background: white;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
@ -6,6 +6,9 @@
|
||||
overflow:auto;
|
||||
margin-top:20px;
|
||||
}
|
||||
.paddedTable {
|
||||
padding-bottom: 6%;
|
||||
}
|
||||
#table-wrapper table {
|
||||
width:100%;
|
||||
|
||||
@ -32,6 +35,9 @@ div#activeSearches table td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div#topTableSender table thead th:nth-child(1) {
|
||||
width: 35%;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
@ -39,7 +45,10 @@ div#topTableSender table thead th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(4) {
|
||||
width: 340px;
|
||||
width: 100px;
|
||||
}
|
||||
div#topTableSender table thead th:nth-child(5) {
|
||||
width: 35%;
|
||||
}
|
||||
div#topTableSender table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
@ -97,6 +106,9 @@ div#bottomTableFile table thead th:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
div#bottomTableFile table thead th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
div#bottomTableFile table thead th:nth-child(4) {
|
||||
width: 340px;
|
||||
}
|
||||
div#bottomTableFile table tbody td:nth-child(1) {
|
||||
@ -128,6 +140,7 @@ div#filesTable table thead th:nth-child(2) {
|
||||
}
|
||||
div#filesTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
div#filesTable table tbody td:nth-child(2) {
|
||||
padding-right: 25px;
|
||||
@ -196,6 +209,81 @@ div#downloadDetails table td {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div#feedConfig table * {
|
||||
background: #bcd1e5 !important;
|
||||
}
|
||||
div#feedConfig table {
|
||||
border-collapse: collapse;
|
||||
width: auto;
|
||||
}
|
||||
div#feedConfig table td {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div#feedsTable table thead th:nth-child(2) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
div#feedsTable table thead th:nth-child(3) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#feedsTable table thead th:nth-child(4) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
div#feedsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div#itemsTable table thead th:nth-child(2) {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
div#itemsTable table thead th:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
div#itemsTable table thead th:nth-child(4) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#itemsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#hitsTable table thead th:nth-child(1) {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
div#hitsTable table thead th:nth-child(2) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#hitsTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#hitsTable table tbody td:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#certificatesTable table thead th:nth-child(2) {
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
div#certificatesTable table tbody td:nth-child(1) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#certificatesTable table tbody td:nth-child(3) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div#uploads table thead th:nth-child(2) {
|
||||
width: 120px;
|
||||
}
|
||||
@ -370,6 +458,10 @@ pre.comment {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
pre.fullId {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* File tree CSS */
|
||||
|
||||
/* Remove default bullets */
|
||||
@ -413,3 +505,58 @@ ul, #sharedTree {
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul.fileTree {
|
||||
margin-left: -1.5%;
|
||||
}
|
||||
|
||||
li.fileTree {
|
||||
|
||||
}
|
||||
|
||||
.accordion {
|
||||
cursor: pointer;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.panel {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height: 0.2s ease-out;
|
||||
}
|
||||
|
||||
.droplink {
|
||||
cursor : pointer;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index:1;
|
||||
background: #f5f5f5;
|
||||
padding-left: 20px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Change color of dropdown links on hover */
|
||||
.dropdown-content a:hover {background-color: #ddd;}
|
||||
|
||||
/* Show the dropdown menu on hover */
|
||||
.dropdown:hover .dropdown-content {display: block;}
|
||||
|
||||
textarea.copypaste {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
z-index: -9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
BIN
webui/src/main/images/AboutMe.png
Normal file
After Width: | Height: | Size: 849 B |
BIN
webui/src/main/images/BrowseHost.png
Normal file
After Width: | Height: | Size: 681 B |
BIN
webui/src/main/images/Chat.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
webui/src/main/images/ConfigurationPage.png
Normal file
After Width: | Height: | Size: 873 B |
BIN
webui/src/main/images/Connect.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
webui/src/main/images/Downloads.png
Normal file
After Width: | Height: | Size: 749 B |
BIN
webui/src/main/images/Feeds.png
Normal file
After Width: | Height: | Size: 897 B |
BIN
webui/src/main/images/MuStatus.png
Normal file
After Width: | Height: | Size: 576 B |
BIN
webui/src/main/images/NotStarted.png
Normal file
After Width: | Height: | Size: 895 B |
BIN
webui/src/main/images/Search.png
Normal file
After Width: | Height: | Size: 782 B |
BIN
webui/src/main/images/SharedFiles.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
webui/src/main/images/TrustList.png
Normal file
After Width: | Height: | Size: 882 B |
BIN
webui/src/main/images/TrustUsers.png
Normal file
After Width: | Height: | Size: 942 B |
Before Width: | Height: | Size: 280 B |
Before Width: | Height: | Size: 328 B |
Before Width: | Height: | Size: 290 B |
Before Width: | Height: | Size: 272 B |
Before Width: | Height: | Size: 421 B |
Before Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 393 B |
Before Width: | Height: | Size: 213 B |
Before Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 199 B |