Compare commits

..

43 Commits

Author SHA1 Message Date
Zlatin Balevsky
337605dc0f Release 0.4.15 2019-10-10 16:48:10 +01:00
Zlatin Balevsky
14bdfa6b2e throttle even further - 500/s 2019-10-09 17:34:54 +01:00
Zlatin Balevsky
ed3f9da773 throttle loading even further, to 1000/sec 2019-10-09 16:46:17 +01:00
Zlatin Balevsky
251080d08f throttle loading of files to 500/s 2019-10-09 16:34:09 +01:00
Zlatin Balevsky
f530ab999d operations on multiple selection in shared files table 2019-10-09 03:38:08 +01:00
Zlatin Balevsky
4133384e48 ability to share multiple files and directories 2019-10-08 21:30:34 +01:00
Zlatin Balevsky
600fc98868 update TODO 2019-10-07 12:38:26 +01:00
Zlatin Balevsky
129eeb3b88 JDK needed, not JRE 2019-10-07 12:38:09 +01:00
Zlatin Balevsky
20b51b78a0 reduce priority of file persister thread 2019-10-07 11:59:51 +01:00
Zlatin Balevsky
33fe755b60 implement multiple-selection on downloads table 2019-10-07 04:26:35 +01:00
Zlatin Balevsky
8b0668a134 Rewrite utils into Java, cache the persistable data of shared files to reduce object churn 2019-10-05 22:50:32 +01:00
Zlatin Balevsky
730d2202fd bundles for linux available now 2019-10-05 18:53:43 +01:00
Zlatin Balevsky
69906a986d set i2p.dir.base to prevent router creating files in PWD 2019-10-05 15:03:59 +01:00
Zlatin Balevsky
5bc8fa8633 Preserve selection on refresh #18 2019-10-05 05:13:49 +01:00
Zlatin Balevsky
7de7c9d8f3 Add 'Clear Hits' button to content control panel #18 2019-10-05 05:03:25 +01:00
Zlatin Balevsky
e943f6019d disable all GUI unit tests, enable host-cache unit tests. The 'build' target now succeeds 2019-10-05 04:31:11 +01:00
Zlatin Balevsky
2eec7bec5b fix most core tests 2019-10-05 04:20:14 +01:00
Zlatin Balevsky
c36110cf76 update readme 2019-10-04 16:41:07 +01:00
Zlatin Balevsky
abe28517bc Release 0.4.14 2019-10-04 13:00:57 +01:00
Zlatin Balevsky
15bc4c064d center the button 2019-10-03 21:32:32 +01:00
Zlatin Balevsky
91d771944b add option for sequential download 2019-10-03 20:45:22 +01:00
Zlatin Balevsky
e09c456a13 make the download retry interval in seconds, default still 1 minute 2019-10-03 19:31:15 +01:00
Zlatin Balevsky
d9c1067226 Add Neutral button to search tab, issue #17 2019-10-02 06:02:06 +01:00
Zlatin Balevsky
eda3e7ad3a Add option to not search extra hop, only considered if connecting only to trusted peers, issue #6 2019-10-02 05:45:46 +01:00
Zlatin Balevsky
e9798c7eaa remember last rejection and back off from hosts that reject us. Fix return value of retry and hopelessness predicates 2019-10-01 08:34:43 +01:00
Zlatin Balevsky
66bb4eef5b close outbound establishments on a separate thread 2019-10-01 07:50:29 +01:00
Zlatin Balevsky
55f260b3f4 update version 2019-09-29 19:21:06 +01:00
Zlatin Balevsky
32d4c3965e Release 0.4.13 2019-09-29 19:00:20 +01:00
Zlatin Balevsky
de1534d837 reduce the default host retry interval 2019-09-29 18:45:09 +01:00
Zlatin Balevsky
7b58e8a88a separate setting for the interval after which a host is considered hopeless 2019-09-29 18:43:39 +01:00
Zlatin Balevsky
8a03b89985 clean up the filtering logic; allow serialization of hosts that can be retried 2019-09-29 16:49:02 +01:00
Zlatin Balevsky
1d97374857 track last successful attempt. Only re-attempt hosts if they have ever been successful. Do not serialize hosts considered hopeless 2019-09-29 16:19:19 +01:00
Zlatin Balevsky
549e8c2d98 Release 0.4.12 2019-09-22 16:55:04 +01:00
Zlatin Balevsky
b54d24db0d new update server destination 2019-09-22 16:47:35 +01:00
Zlatin Balevsky
fa12e84345 stronger sig type 2019-09-22 16:23:01 +01:00
Zlatin Balevsky
6430ff2691 bump i2p libs version 2019-09-22 16:13:12 +01:00
Zlatin Balevsky
591313c81c point to the pkg project 2019-09-20 21:09:53 +01:00
Zlatin Balevsky
ce7b6a0c65 change to gasp AA font table, try metal lnf if the others fail 2019-09-16 15:06:45 +01:00
Zlatin Balevsky
5c4d4c4580 embedded router will not work without reseed certificates, so remove it 2019-09-16 15:04:34 +01:00
Zlatin Balevsky
4cb864ff9f update version 2019-09-16 15:03:20 +01:00
Zlatin Balevsky
417675ad07 update dark_trion's hostcache address 2019-07-22 21:48:29 +01:00
Zlatin Balevsky
9513e5ba3c update todo 2019-07-20 13:15:44 +01:00
Zlatin Balevsky
85610cf169 add new host-cache 2019-07-15 22:05:09 +01:00
53 changed files with 509 additions and 411 deletions

View File

@@ -4,11 +4,11 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.4.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder. The current stable release - 0.4.14 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building ### Building
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type You need JDK 8 or newer. After installing that and setting up the appropriate paths, just type
``` ```
./gradlew clean assemble ./gradlew clean assemble
@@ -19,18 +19,16 @@ If you want to run the unit tests, type
./gradlew clean build ./gradlew clean build
``` ```
Some of the UI tests will fail because they haven't been written yet :-/ If you want to build binary bundles that do not depend on Java or I2P, see the https://github.com/zlatinb/muwire-pkg project
### Running ### Running
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt. After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar gui-x.y.z.jar` in a terminal or command prompt.
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire` If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there. On Windows that file should go into `%HOME%\AppData\Roaming\MuWire`, on Mac into `$HOME/Library/Application Support/MuWire` and on Linux `$HOME/.MuWire`
[Default I2CP port]\: `7654` [Default I2CP port]\: `7654`
If you do not have an I2P router, pass the following switch to the Java process: `-DembeddedRouter=true`. This will launch MuWire's embedded router. Be aware that this causes startup to take a lot longer.
### GPG Fingerprint ### GPG Fingerprint
``` ```

View File

@@ -12,10 +12,6 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability This helps with scalability
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
##### Web UI, REST Interface, etc. ##### Web UI, REST Interface, etc.
Basically any non-gui non-cli user interface Basically any non-gui non-cli user interface
@@ -27,5 +23,4 @@ To enable parsing of metadata from known file types and the user editing it or a
### Small Items ### Small Items
* Wrapper of some kind for in-place upgrades * Wrapper of some kind for in-place upgrades
* Download file sequentially * Automatic adjustment of number of I2P tunnels
* Multiple-selection download, Ctrl-A

View File

@@ -2,7 +2,7 @@ subprojects {
apply plugin: 'groovy' apply plugin: 'groovy'
dependencies { dependencies {
compile 'net.i2p:i2p:0.9.41' compile 'net.i2p:i2p:0.9.42'
compile 'org.codehaus.groovy:groovy-all:2.4.15' compile 'org.codehaus.groovy:groovy-all:2.4.15'
} }

View File

@@ -35,7 +35,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.4.11") core = new Core(props, home, "0.4.15")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

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

View File

@@ -2,9 +2,9 @@ apply plugin : 'application'
mainClassName = 'com.muwire.core.Core' mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies { dependencies {
compile 'net.i2p:router:0.9.41' compile 'net.i2p:router:0.9.42'
compile 'net.i2p.client:mstreaming:0.9.41' compile 'net.i2p.client:mstreaming:0.9.42'
compile 'net.i2p.client:streaming:0.9.41' compile 'net.i2p.client:streaming:0.9.42'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2' testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'

View File

@@ -1,13 +0,0 @@
package com.muwire.core
import net.i2p.crypto.SigType
class Constants {
public static final byte PERSONA_VERSION = (byte)1
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519
public static final int MAX_HEADER_SIZE = 0x1 << 14
public static final int MAX_HEADERS = 16
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]"
}

View File

@@ -135,6 +135,7 @@ public class Core {
} else { } else {
log.info("launching embedded router") log.info("launching embedded router")
Properties routerProps = new Properties() Properties routerProps = new Properties()
routerProps.setProperty("i2p.dir.base", home.getAbsolutePath())
routerProps.setProperty("i2p.dir.config", home.getAbsolutePath()) routerProps.setProperty("i2p.dir.config", home.getAbsolutePath())
routerProps.setProperty("router.excludePeerCaps", "KLM") routerProps.setProperty("router.excludePeerCaps", "KLM")
routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw)) routerProps.setProperty("i2np.inboundKBytesPerSecond", String.valueOf(props.inBw))
@@ -361,7 +362,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.4.11") Core core = new Core(props, home, "0.4.15")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -11,6 +11,7 @@ class MuWireSettings {
final boolean isLeaf final boolean isLeaf
boolean allowUntrusted boolean allowUntrusted
boolean searchExtraHop
boolean allowTrustLists boolean allowTrustLists
int trustListInterval int trustListInterval
Set<Persona> trustSubscriptions Set<Persona> trustSubscriptions
@@ -24,7 +25,7 @@ class MuWireSettings {
boolean shareDownloadedFiles boolean shareDownloadedFiles
Set<String> watchedDirectories Set<String> watchedDirectories
float downloadSequentialRatio float downloadSequentialRatio
int hostClearInterval int hostClearInterval, hostHopelessInterval, hostRejectInterval
int meshExpiration int meshExpiration
boolean embeddedRouter boolean embeddedRouter
int inBw, outBw int inBw, outBw
@@ -38,19 +39,22 @@ class MuWireSettings {
MuWireSettings(Properties props) { MuWireSettings(Properties props) {
isLeaf = Boolean.valueOf(props.get("leaf","false")) isLeaf = Boolean.valueOf(props.get("leaf","false"))
allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true")) allowUntrusted = Boolean.valueOf(props.getProperty("allowUntrusted","true"))
searchExtraHop = Boolean.valueOf(props.getProperty("searchExtraHop","false"))
allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true")) allowTrustLists = Boolean.valueOf(props.getProperty("allowTrustLists","true"))
trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1")) trustListInterval = Integer.valueOf(props.getProperty("trustListInterval","1"))
crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED")) crawlerResponse = CrawlerResponse.valueOf(props.get("crawlerResponse","REGISTERED"))
nickname = props.getProperty("nickname","MuWireUser") nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","1")) downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24")) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true")) autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
updateType = props.getProperty("updateType","jar") updateType = props.getProperty("updateType","jar")
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8")) downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","60")) hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60")) meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false")) embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256")) inBw = Integer.valueOf(props.getProperty("inBw","256"))
@@ -74,6 +78,7 @@ class MuWireSettings {
Properties props = new Properties() Properties props = new Properties()
props.setProperty("leaf", isLeaf.toString()) props.setProperty("leaf", isLeaf.toString())
props.setProperty("allowUntrusted", allowUntrusted.toString()) props.setProperty("allowUntrusted", allowUntrusted.toString())
props.setProperty("searchExtraHop", String.valueOf(searchExtraHop))
props.setProperty("allowTrustLists", String.valueOf(allowTrustLists)) props.setProperty("allowTrustLists", String.valueOf(allowTrustLists))
props.setProperty("trustListInterval", String.valueOf(trustListInterval)) props.setProperty("trustListInterval", String.valueOf(trustListInterval))
props.setProperty("crawlerResponse", crawlerResponse.toString()) props.setProperty("crawlerResponse", crawlerResponse.toString())
@@ -86,6 +91,8 @@ class MuWireSettings {
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles)) props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio)) props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval)) props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration)) props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter)) props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("inBw", String.valueOf(inBw)) props.setProperty("inBw", String.valueOf(inBw))

View File

@@ -0,0 +1,7 @@
package com.muwire.core
class SplitPattern {
public static final String SPLIT_PATTERN = "[\\*\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|\\[\\]\\{\\}\\?]";
}

View File

@@ -31,7 +31,7 @@ class ConnectionEstablisher {
final HostCache hostCache final HostCache hostCache
final Timer timer final Timer timer
final ExecutorService executor final ExecutorService executor, closer
final Set inProgress = new ConcurrentHashSet() final Set inProgress = new ConcurrentHashSet()
@@ -51,6 +51,8 @@ class ConnectionEstablisher {
rv.setName("connector-${System.currentTimeMillis()}") rv.setName("connector-${System.currentTimeMillis()}")
rv rv
} as ThreadFactory) } as ThreadFactory)
closer = Executors.newSingleThreadExecutor()
} }
void start() { void start() {
@@ -60,6 +62,7 @@ class ConnectionEstablisher {
void stop() { void stop() {
timer.cancel() timer.cancel()
executor.shutdownNow() executor.shutdownNow()
closer.shutdown()
} }
private void connectIfNeeded() { private void connectIfNeeded() {
@@ -120,8 +123,10 @@ class ConnectionEstablisher {
} }
private void fail(Endpoint endpoint) { private void fail(Endpoint endpoint) {
endpoint.close() closer.execute {
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED)) endpoint.close()
eventBus.publish(new ConnectionEvent(endpoint: endpoint, incoming: false, leaf: false, status: ConnectionAttemptStatus.FAILED))
} as Runnable
} }
private void readK(Endpoint e) { private void readK(Endpoint e) {
@@ -175,7 +180,7 @@ class ConnectionEstablisher {
log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore) log.log(Level.WARNING,"Problem parsing post-rejection payload",ignore)
} finally { } finally {
// the end // the end
e.close() closer.execute({e.close()} as Runnable)
} }
} }

View File

@@ -74,7 +74,7 @@ public class DownloadManager {
destinations.addAll(e.sources) destinations.addAll(e.sources)
destinations.remove(me.destination) destinations.remove(me.destination)
Pieces pieces = getPieces(infohash, size, pieceSize) Pieces pieces = getPieces(infohash, size, pieceSize, e.sequential)
def downloader = new Downloader(eventBus, this, me, e.target, size, def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations, infohash, pieceSize, connector, destinations,
@@ -122,8 +122,12 @@ public class DownloadManager {
byte [] root = Base64.decode(json.hashRoot) byte [] root = Base64.decode(json.hashRoot)
infoHash = new InfoHash(root) infoHash = new InfoHash(root)
} }
boolean sequential = false
if (json.sequential != null)
sequential = json.sequential
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2) Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, me, file, (long)json.length, def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces) infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
@@ -137,12 +141,12 @@ public class DownloadManager {
} }
} }
private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2) { private Pieces getPieces(InfoHash infoHash, long length, int pieceSizePow2, boolean sequential) {
int pieceSize = 0x1 << pieceSizePow2 int pieceSize = 0x1 << pieceSizePow2
int nPieces = (int)(length / pieceSize) int nPieces = (int)(length / pieceSize)
if (length % pieceSize != 0) if (length % pieceSize != 0)
nPieces++ nPieces++
Mesh mesh = meshManager.getOrCreate(infoHash, nPieces) Mesh mesh = meshManager.getOrCreate(infoHash, nPieces, sequential)
mesh.pieces mesh.pieces
} }
@@ -188,6 +192,9 @@ public class DownloadManager {
json.hashRoot = Base64.encode(infoHash.getRoot()) json.hashRoot = Base64.encode(infoHash.getRoot())
json.paused = downloader.paused json.paused = downloader.paused
json.sequential = downloader.pieces.ratio == 0f
writer.println(JsonOutput.toJson(json)) writer.println(JsonOutput.toJson(json))
} }
} }

View File

@@ -17,7 +17,7 @@ class Pieces {
done = new BitSet(nPieces) done = new BitSet(nPieces)
claimed = new BitSet(nPieces) claimed = new BitSet(nPieces)
} }
synchronized int[] claim() { synchronized int[] claim() {
int claimedCardinality = claimed.cardinality() int claimedCardinality = claimed.cardinality()
if (claimedCardinality == nPieces) { if (claimedCardinality == nPieces) {
@@ -30,7 +30,7 @@ class Pieces {
} }
// if fuller than ratio just do sequential // if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces > ratio) { if ( (1.0f * claimedCardinality) / nPieces >= ratio) {
int rv = claimed.nextClearBit(0) int rv = claimed.nextClearBit(0)
claimed.set(rv) claimed.set(rv)
return [rv, partials.getOrDefault(rv, 0), 0] return [rv, partials.getOrDefault(rv, 0), 0]
@@ -59,7 +59,8 @@ class Pieces {
return [rv, partials.getOrDefault(rv, 0), 1] return [rv, partials.getOrDefault(rv, 0), 1]
} }
List<Integer> toList = availableCopy.toList() List<Integer> toList = availableCopy.toList()
Collections.shuffle(toList) if (ratio > 0f)
Collections.shuffle(toList)
int rv = toList[0] int rv = toList[0]
claimed.set(rv) claimed.set(rv)
[rv, partials.getOrDefault(rv, 0), 0] [rv, partials.getOrDefault(rv, 0), 0]

View File

@@ -10,4 +10,5 @@ class UIDownloadEvent extends Event {
UIResultEvent[] result UIResultEvent[] result
Set<Destination> sources Set<Destination> sources
File target File target
boolean sequential
} }

View File

@@ -46,7 +46,10 @@ class PersisterService extends Service {
} }
void load() { void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
if (location.exists() && location.isFile()) { if (location.exists() && location.isFile()) {
int loaded = 0
def slurper = new JsonSlurper() def slurper = new JsonSlurper()
try { try {
location.eachLine { location.eachLine {
@@ -56,6 +59,9 @@ class PersisterService extends Service {
if (event != null) { if (event != null) {
log.fine("loaded file $event.loadedFile.file") log.fine("loaded file $event.loadedFile.file")
listener.publish event listener.publish event
loaded++
if (loaded % 10 == 0)
Thread.sleep(20)
} }
} }
} }
@@ -136,17 +142,12 @@ class PersisterService extends Service {
private def toJson(File f, SharedFile sf) { private def toJson(File f, SharedFile sf) {
def json = [:] def json = [:]
json.file = Base64.encode DataUtil.encodei18nString(f.toString()) json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength() json.length = sf.getCachedLength()
InfoHash ih = sf.getInfoHash() InfoHash ih = sf.getInfoHash()
json.infoHash = Base64.encode ih.getRoot() json.infoHash = sf.getB64EncodedHashRoot()
json.pieceSize = sf.getPieceSize() json.pieceSize = sf.getPieceSize()
byte [] tmp = new byte [32] json.hashList = sf.getB64EncodedHashList()
json.hashList = []
for (int i = 0;i < ih.getHashList().length / 32; i++) {
System.arraycopy(ih.getHashList(), i * 32, tmp, 0, 32)
json.hashList.add Base64.encode(tmp)
}
if (sf instanceof DownloadedFile) { if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList()) json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())

View File

@@ -6,8 +6,12 @@ class CacheServers {
private static final int TO_GIVE = 3 private static final int TO_GIVE = 3
private static Set<Destination> CACHES = [ private static Set<Destination> CACHES = [
// zlatinb
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"), new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA==") // sNL
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA=="),
// dark_trion
new Destination("Gec9L29FVcQvYDgpcYuEYdltJn06PPoOWAcAM8Af-gDm~ehlrJcwlLXXs0hidq~yP2A0X7QcDi6i6shAfuEofTchxGJl8LRNqj9lio7WnB7cIixXWL~uCkD7Np5LMX0~akNX34oOb9RcBYVT2U5rFGJmJ7OtBv~IBkGeLhsMrqaCjahd0jdBO~QJ-t82ZKZhh044d24~JEfF9zSJxdBoCdAcXzryGNy7sYtFVDFsPKJudAxSW-UsSQiGw2~k-TxyF0r-iAt1IdzfNu8Lu0WPqLdhDYJWcPldx2PR5uJorI~zo~z3I5RX3NwzarlbD4nEP5s65ahPSfVCEkzmaJUBgP8DvBqlFaX89K4nGRYc7jkEjJ8cX4L6YPXUpTPWcfKkW259WdQY3YFh6x7rzijrGZewpczOLCrt-bZRYgDrUibmZxKZmNhy~lQu4gYVVjkz1i4tL~DWlhIc4y0x2vItwkYLArPPi~ejTnt-~Lhb7oPMXRcWa3UrwGKpFvGZY4NXBQAEAAcAAA==")
] ]
static List<Destination> getCacheServers() { static List<Destination> getCacheServers() {

View File

@@ -7,21 +7,35 @@ class Host {
private static final int MAX_FAILURES = 3 private static final int MAX_FAILURES = 3
final Destination destination final Destination destination
private final int clearInterval private final int clearInterval, hopelessInterval, rejectionInterval
int failures,successes int failures,successes
long lastAttempt long lastAttempt
long lastSuccessfulAttempt
long lastRejection
public Host(Destination destination, int clearInterval) { public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
this.destination = destination this.destination = destination
this.clearInterval = clearInterval this.clearInterval = clearInterval
this.hopelessInterval = hopelessInterval
this.rejectionInterval = rejectionInterval
} }
synchronized void onConnect() { private void connectSuccessful() {
failures = 0 failures = 0
successes++ successes++
lastAttempt = System.currentTimeMillis() lastAttempt = System.currentTimeMillis()
} }
synchronized void onConnect() {
connectSuccessful()
lastSuccessfulAttempt = lastAttempt
}
synchronized void onReject() {
connectSuccessful()
lastRejection = lastAttempt;
}
synchronized void onFailure() { synchronized void onFailure() {
failures++ failures++
successes = 0 successes = 0
@@ -40,7 +54,17 @@ class Host {
failures = 0 failures = 0
} }
synchronized void canTryAgain() { synchronized boolean canTryAgain() {
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000) lastSuccessfulAttempt > 0 &&
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
}
synchronized boolean isHopeless() {
isFailed() &&
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
}
synchronized boolean isRecentlyRejected() {
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
} }
} }

View File

@@ -52,7 +52,7 @@ class HostCache extends Service {
hosts.get(e.destination).clearFailures() hosts.get(e.destination).clearFailures()
return return
} }
Host host = new Host(e.destination, settings.hostClearInterval) Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
if (allowHost(host)) { if (allowHost(host)) {
hosts.put(e.destination, host) hosts.put(e.destination, host)
} }
@@ -64,15 +64,17 @@ class HostCache extends Service {
Destination dest = e.endpoint.destination Destination dest = e.endpoint.destination
Host host = hosts.get(dest) Host host = hosts.get(dest)
if (host == null) { if (host == null) {
host = new Host(dest, settings.hostClearInterval) host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
hosts.put(dest, host) hosts.put(dest, host)
} }
switch(e.status) { switch(e.status) {
case ConnectionAttemptStatus.SUCCESSFUL: case ConnectionAttemptStatus.SUCCESSFUL:
case ConnectionAttemptStatus.REJECTED:
host.onConnect() host.onConnect()
break break
case ConnectionAttemptStatus.REJECTED:
host.onReject()
break
case ConnectionAttemptStatus.FAILED: case ConnectionAttemptStatus.FAILED:
host.onFailure() host.onFailure()
break break
@@ -82,6 +84,10 @@ class HostCache extends Service {
List<Destination> getHosts(int n) { List<Destination> getHosts(int n) {
List<Destination> rv = new ArrayList<>(hosts.keySet()) List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {allowHost(hosts[it])} rv.retainAll {allowHost(hosts[it])}
rv.removeAll {
def h = hosts[it];
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected()
}
if (rv.size() <= n) if (rv.size() <= n)
return rv return rv
Collections.shuffle(rv) Collections.shuffle(rv)
@@ -106,12 +112,16 @@ class HostCache extends Service {
storage.eachLine { storage.eachLine {
def entry = slurper.parseText(it) def entry = slurper.parseText(it)
Destination dest = new Destination(entry.destination) Destination dest = new Destination(entry.destination)
Host host = new Host(dest, settings.hostClearInterval) Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
host.failures = Integer.valueOf(String.valueOf(entry.failures)) host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes)) host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null) if (entry.lastAttempt != null)
host.lastAttempt = entry.lastAttempt host.lastAttempt = entry.lastAttempt
if (allowHost(host)) if (entry.lastSuccessfulAttempt != null)
host.lastSuccessfulAttempt = entry.lastSuccessfulAttempt
if (entry.lastRejection != null)
host.lastRejection = entry.lastRejection
if (allowHost(host))
hosts.put(dest, host) hosts.put(dest, host)
} }
} }
@@ -120,8 +130,6 @@ class HostCache extends Service {
} }
private boolean allowHost(Host host) { private boolean allowHost(Host host) {
if (host.isFailed() && !host.canTryAgain())
return false
if (host.destination == myself) if (host.destination == myself)
return false return false
TrustLevel trust = trustService.getLevel(host.destination) TrustLevel trust = trustService.getLevel(host.destination)
@@ -140,12 +148,14 @@ class HostCache extends Service {
storage.delete() storage.delete()
storage.withPrintWriter { writer -> storage.withPrintWriter { writer ->
hosts.each { dest, host -> hosts.each { dest, host ->
if (allowHost(host)) { if (allowHost(host) && !host.isHopeless()) {
def map = [:] def map = [:]
map.destination = dest.toBase64() map.destination = dest.toBase64()
map.failures = host.failures map.failures = host.failures
map.successes = host.successes map.successes = host.successes
map.lastAttempt = host.lastAttempt map.lastAttempt = host.lastAttempt
map.lastSuccessfulAttempt = host.lastSuccessfulAttempt
map.lastRejection = host.lastRejection
def json = JsonOutput.toJson(map) def json = JsonOutput.toJson(map)
writer.println json writer.println json
} }

View File

@@ -33,11 +33,12 @@ class MeshManager {
meshes.get(infoHash) meshes.get(infoHash)
} }
Mesh getOrCreate(InfoHash infoHash, int nPieces) { Mesh getOrCreate(InfoHash infoHash, int nPieces, boolean sequential) {
synchronized(meshes) { synchronized(meshes) {
if (meshes.containsKey(infoHash)) if (meshes.containsKey(infoHash))
return meshes.get(infoHash) return meshes.get(infoHash)
Pieces pieces = new Pieces(nPieces, settings.downloadSequentialRatio) float ratio = sequential ? 0f : settings.downloadSequentialRatio
Pieces pieces = new Pieces(nPieces, ratio)
if (fileManager.rootToFiles.containsKey(infoHash)) { if (fileManager.rootToFiles.containsKey(infoHash)) {
for (int i = 0; i < nPieces; i++) for (int i = 0; i < nPieces; i++)
pieces.markDownloaded(i) pieces.markDownloaded(i)

View File

@@ -1,6 +1,6 @@
package com.muwire.core.search package com.muwire.core.search
import com.muwire.core.Constants import com.muwire.core.SplitPattern
class SearchIndex { class SearchIndex {
@@ -32,7 +32,7 @@ class SearchIndex {
} }
private static String[] split(String source) { private static String[] split(String source) {
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase() source = source.replaceAll(SplitPattern.SPLIT_PATTERN, " ").toLowerCase()
String [] split = source.split(" ") String [] split = source.split(" ")
def rv = [] def rv = []
split.each { if (it.length() > 0) rv << it } split.each { if (it.length() > 0) rv << it }

View File

@@ -3,5 +3,5 @@ package com.muwire.core.update
import net.i2p.data.Destination import net.i2p.data.Destination
class UpdateServers { class UpdateServers {
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA") static final Destination UPDATE_SERVER = new Destination("VJYAiCPZHNLraWvLkeRLxRiT4PHAqNqRO1nH240r7u1noBw8Pa~-lJOhKR7CccPkEN8ejSi4H6XjqKYLC8BKLVLeOgnAbedUVx81MV7DETPDdPEGV4RVu6YDFri7-tJOeqauGHxtlXT44YWuR69xKrTG3u4~iTWgxKnlBDht9Q3aVpSPFD2KqEizfVxolqXI0zmAZ2xMi8jfl0oe4GbgHrD9hR2FYj6yKfdqcUgHVobY4kDdJt-u31QqwWdsQMEj8Y3tR2XcNaITEVPiAjoKgBrYwB4jddWPNaT4XdHz76d9p9Iqes7dhOKq3OKpk6kg-bfIKiEOiA1mY49fn5h8pNShTqV7QBhh4CE4EDT3Szl~WsLdrlHUKJufSi7erEMh3coF7HORpF1wah2Xw7q470t~b8dKGKi7N7xQsqhGruDm66PH9oE9Kt9WBVBq2zORdPRtRM61I7EnrwDlbOkL0y~XpvQ3JKUQKdBQ3QsOJt8CHlhHHXMMbvqhntR61RSDBQAEAAcAAA==")
} }

View File

@@ -92,7 +92,7 @@ public class UploadManager {
pieceSize = downloader.pieceSizePow2 pieceSize = downloader.pieceSizePow2
} else { } else {
SharedFile sharedFile = sharedFiles.iterator().next(); SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces) mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file file = sharedFile.file
pieceSize = sharedFile.pieceSize pieceSize = sharedFile.pieceSize
} }
@@ -217,7 +217,7 @@ public class UploadManager {
pieceSize = downloader.pieceSizePow2 pieceSize = downloader.pieceSizePow2
} else { } else {
SharedFile sharedFile = sharedFiles.iterator().next(); SharedFile sharedFile = sharedFiles.iterator().next();
mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces) mesh = meshManager.getOrCreate(request.infoHash, sharedFile.NPieces, false)
file = sharedFile.file file = sharedFile.file
pieceSize = sharedFile.pieceSize pieceSize = sharedFile.pieceSize
} }

View File

@@ -0,0 +1,11 @@
package com.muwire.core;
import net.i2p.crypto.SigType;
public class Constants {
public static final byte PERSONA_VERSION = (byte)1;
public static final SigType SIG_TYPE = SigType.EdDSA_SHA512_Ed25519;
public static final int MAX_HEADER_SIZE = 0x1 << 14;
public static final int MAX_HEADERS = 16;
}

View File

@@ -2,6 +2,12 @@ package com.muwire.core;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.muwire.core.util.DataUtil;
import net.i2p.data.Base64;
public class SharedFile { public class SharedFile {
@@ -11,6 +17,10 @@ public class SharedFile {
private final String cachedPath; private final String cachedPath;
private final long cachedLength; private final long cachedLength;
private final String b64EncodedFileName;
private final String b64EncodedHashRoot;
private final List<String> b64EncodedHashList;
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException { public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
this.file = file; this.file = file;
@@ -18,6 +28,16 @@ public class SharedFile {
this.pieceSize = pieceSize; this.pieceSize = pieceSize;
this.cachedPath = file.getAbsolutePath(); this.cachedPath = file.getAbsolutePath();
this.cachedLength = file.length(); 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() { public File getFile() {
@@ -40,6 +60,18 @@ public class SharedFile {
rv++; rv++;
return rv; return rv;
} }
public String getB64EncodedFileName() {
return b64EncodedFileName;
}
public String getB64EncodedHashRoot() {
return b64EncodedHashRoot;
}
public List<String> getB64EncodedHashList() {
return b64EncodedHashList;
}
public String getCachedPath() { public String getCachedPath() {
return cachedPath; return cachedPath;

View File

@@ -1,122 +1,134 @@
package com.muwire.core.util package com.muwire.core.util;
import java.lang.reflect.Field import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method import java.io.DataOutputStream;
import java.nio.ByteBuffer import java.io.IOException;
import java.nio.charset.StandardCharsets import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import com.muwire.core.Constants import com.muwire.core.Constants;
import net.i2p.data.Base64 import net.i2p.data.Base64;
class DataUtil { public class DataUtil {
private final static int MAX_SHORT = (0x1 << 16) - 1 private final static int MAX_SHORT = (0x1 << 16) - 1;
static void writeUnsignedShort(int value, OutputStream os) { static void writeUnsignedShort(int value, OutputStream os) throws IOException {
if (value > MAX_SHORT || value < 0) if (value > MAX_SHORT || value < 0)
throw new IllegalArgumentException("$value invalid") throw new IllegalArgumentException("$value invalid");
byte lsb = (byte) (value & 0xFF) byte lsb = (byte) (value & 0xFF);
byte msb = (byte) (value >> 8) byte msb = (byte) (value >> 8);
os.write(msb) os.write(msb);
os.write(lsb) os.write(lsb);
} }
private final static int MAX_HEADER = 0x7FFFFF private final static int MAX_HEADER = 0x7FFFFF;
static void packHeader(int length, byte [] header) { static void packHeader(int length, byte [] header) {
if (header.length != 3) if (header.length != 3)
throw new IllegalArgumentException("header length $header.length") throw new IllegalArgumentException("header length $header.length");
if (length < 0 || length > MAX_HEADER) if (length < 0 || length > MAX_HEADER)
throw new IllegalArgumentException("length $length") throw new IllegalArgumentException("length $length");
header[2] = (byte) (length & 0xFF) header[2] = (byte) (length & 0xFF);
header[1] = (byte) ((length >> 8) & 0xFF) header[1] = (byte) ((length >> 8) & 0xFF);
header[0] = (byte) ((length >> 16) & 0x7F) header[0] = (byte) ((length >> 16) & 0x7F);
} }
static int readLength(byte [] header) { static int readLength(byte [] header) {
if (header.length != 3) if (header.length != 3)
throw new IllegalArgumentException("header length $header.length") throw new IllegalArgumentException("header length $header.length");
return (((int)(header[0] & 0x7F)) << 16) | return (((int)(header[0] & 0x7F)) << 16) |
(((int)(header[1] & 0xFF) << 8)) | (((int)(header[1] & 0xFF) << 8)) |
((int)header[2] & 0xFF) ((int)header[2] & 0xFF);
} }
static String readi18nString(byte [] encoded) { static String readi18nString(byte [] encoded) {
if (encoded.length < 2) if (encoded.length < 2)
throw new IllegalArgumentException("encoding too short $encoded.length") throw new IllegalArgumentException("encoding too short $encoded.length");
int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF) int length = ((encoded[0] & 0xFF) << 8) | (encoded[1] & 0xFF);
if (encoded.length != length + 2) if (encoded.length != length + 2)
throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length") throw new IllegalArgumentException("encoding doesn't match length, expected $length found $encoded.length");
byte [] string = new byte[length] byte [] string = new byte[length];
System.arraycopy(encoded, 2, string, 0, length) System.arraycopy(encoded, 2, string, 0, length);
new String(string, StandardCharsets.UTF_8) return new String(string, StandardCharsets.UTF_8);
} }
static byte[] encodei18nString(String string) { public static byte[] encodei18nString(String string) {
byte [] utf8 = string.getBytes(StandardCharsets.UTF_8) byte [] utf8 = string.getBytes(StandardCharsets.UTF_8);
if (utf8.length > Short.MAX_VALUE) if (utf8.length > Short.MAX_VALUE)
throw new IllegalArgumentException("String in utf8 too long $utf8.length") throw new IllegalArgumentException("String in utf8 too long $utf8.length");
def baos = new ByteArrayOutputStream() ByteArrayOutputStream baos = new ByteArrayOutputStream();
def daos = new DataOutputStream(baos) DataOutputStream daos = new DataOutputStream(baos);
daos.writeShort((short) utf8.length) try {
daos.write(utf8) daos.writeShort((short) utf8.length);
daos.close() daos.write(utf8);
baos.toByteArray() daos.close();
} catch (IOException impossible) {
throw new IllegalStateException(impossible);
}
return baos.toByteArray();
} }
public static String readTillRN(InputStream is) { public static String readTillRN(InputStream is) throws IOException {
def baos = new ByteArrayOutputStream() ByteArrayOutputStream baos = new ByteArrayOutputStream();
while(baos.size() < (Constants.MAX_HEADER_SIZE)) { while(baos.size() < (Constants.MAX_HEADER_SIZE)) {
byte read = is.read() int read = is.read();
if (read == -1) if (read == -1)
throw new IOException() throw new IOException();
if (read == '\r') { if (read == '\r') {
if (is.read() != '\n') if (is.read() != '\n')
throw new IOException("invalid header") throw new IOException("invalid header");
break break;
} }
baos.write(read) baos.write(read);
} }
new String(baos.toByteArray(), StandardCharsets.US_ASCII) return new String(baos.toByteArray(), StandardCharsets.US_ASCII);
} }
public static String encodeXHave(List<Integer> pieces, int totalPieces) { public static String encodeXHave(List<Integer> pieces, int totalPieces) {
int bytes = totalPieces / 8 int bytes = totalPieces / 8;
if (totalPieces % 8 != 0) if (totalPieces % 8 != 0)
bytes++ bytes++;
byte[] raw = new byte[bytes] byte[] raw = new byte[bytes];
pieces.each { for (int it : pieces) {
int byteIdx = it / 8 int byteIdx = it / 8;
int offset = it % 8 int offset = it % 8;
int mask = 0x80 >>> offset int mask = 0x80 >>> offset;
raw[byteIdx] |= mask raw[byteIdx] |= mask;
} }
Base64.encode(raw) return Base64.encode(raw);
} }
public static List<Integer> decodeXHave(String xHave) { public static List<Integer> decodeXHave(String xHave) {
byte [] availablePieces = Base64.decode(xHave) byte [] availablePieces = Base64.decode(xHave);
List<Integer> available = new ArrayList<>() List<Integer> available = new ArrayList<>();
availablePieces.eachWithIndex {b, i -> for (int i = 0; i < availablePieces.length; i ++) {
byte b = availablePieces[i];
for (int j = 0; j < 8 ; j++) { for (int j = 0; j < 8 ; j++) {
byte mask = 0x80 >>> j byte mask = (byte) (0x80 >>> j);
if ((b & mask) == mask) { if ((b & mask) == mask) {
available.add(i * 8 + j) available.add(i * 8 + j);
} }
} }
} }
available return available;
} }
public static Exception findRoot(Exception e) { public static Throwable findRoot(Throwable e) {
while(e.getCause() != null) while(e.getCause() != null)
e = e.getCause() e = e.getCause();
e return e;
} }
public static void tryUnmap(ByteBuffer cb) { public static void tryUnmap(ByteBuffer cb) {

View File

@@ -4,6 +4,7 @@ import static org.junit.Assert.fail
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import com.muwire.core.EventBus import com.muwire.core.EventBus
@@ -180,10 +181,11 @@ class DownloadSessionTest {
} }
@Test @Test
@Ignore // this needs to be rewritten with stealing in mind
public void testSmallFileClaimed() { public void testSmallFileClaimed() {
initSession(20, [0]) initSession(20, [0])
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
downloadThread.join(100) downloadThread.join(150)
assert 100 >= (System.currentTimeMillis() - now) assert 100 >= (System.currentTimeMillis() - now)
assert !performed assert !performed
assert available.isEmpty() assert available.isEmpty()

View File

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

View File

@@ -72,6 +72,9 @@ class HostCacheTest {
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
settingsMock.ignore.allowUntrusted { true } settingsMock.ignore.allowUntrusted { true }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
@@ -91,6 +94,10 @@ class HostCacheTest {
TrustLevel.DISTRUSTED TrustLevel.DISTRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -104,6 +111,9 @@ class HostCacheTest {
TrustLevel.NEUTRAL TrustLevel.NEUTRAL
} }
settingsMock.ignore.allowUntrusted { false } settingsMock.ignore.allowUntrusted { false }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
@@ -123,6 +133,9 @@ class HostCacheTest {
} }
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED } trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED } trustMock.demand.getLevel{ d -> TrustLevel.TRUSTED }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -139,7 +152,15 @@ class HostCacheTest {
assert d == destinations.dest1 assert d == destinations.dest1
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
trustMock.demand.getLevel { d ->
assert d == destinations.dest1
TrustLevel.TRUSTED
}
settingsMock.ignore.getHostClearInterval { 100 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -158,6 +179,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -183,6 +208,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -214,6 +243,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -229,6 +262,11 @@ class HostCacheTest {
assert d == destinations.dest1 assert d == destinations.dest1
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1)) cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
Thread.sleep(150) Thread.sleep(150)
@@ -260,6 +298,10 @@ class HostCacheTest {
TrustLevel.TRUSTED TrustLevel.TRUSTED
} }
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
initMocks() initMocks()
def rv = cache.getHosts(5) def rv = cache.getHosts(5)
assert rv.size() == 1 assert rv.size() == 1

View File

@@ -9,6 +9,9 @@ import org.junit.Test
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import com.muwire.core.download.Pieces
import com.muwire.core.files.FileHasher
import com.muwire.core.mesh.Mesh
class UploaderTest { class UploaderTest {
@@ -52,7 +55,13 @@ class UploaderTest {
} }
private void startUpload() { private void startUpload() {
uploader = new ContentUploader(file, request, endpoint) def hasher = new FileHasher()
InfoHash infoHash = hasher.hashFile(file)
Pieces pieces = new Pieces(FileHasher.getPieceSize(file.length()))
for (int i = 0; i < pieces.nPieces; i++)
pieces.markDownloaded(i)
Mesh mesh = new Mesh(infoHash, pieces)
uploader = new ContentUploader(file, request, endpoint, mesh, FileHasher.getPieceSize(file.length()))
uploadThread = new Thread(uploader.respond() as Runnable) uploadThread = new Thread(uploader.respond() as Runnable)
uploadThread.setDaemon(true) uploadThread.setDaemon(true)
uploadThread.start() uploadThread.start()
@@ -81,6 +90,7 @@ class UploaderTest {
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 0-19" == readUntilRN() assert "Content-Range: 0-19" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
byte [] data = new byte[20] byte [] data = new byte[20]
@@ -96,6 +106,7 @@ class UploaderTest {
startUpload() startUpload()
assert "200 OK" == readUntilRN() assert "200 OK" == readUntilRN()
assert "Content-Range: 5-15" == readUntilRN() assert "Content-Range: 5-15" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
byte [] data = new byte[11] byte [] data = new byte[11]
@@ -111,6 +122,7 @@ class UploaderTest {
request = new ContentRequest(range : new Range(0,20)) request = new ContentRequest(range : new Range(0,20))
startUpload() startUpload()
assert "416 Range Not Satisfiable" == readUntilRN() assert "416 Range Not Satisfiable" == readUntilRN()
assert readUntilRN().startsWith("X-Have")
assert "" == readUntilRN() assert "" == readUntilRN()
} }
@@ -123,6 +135,7 @@ class UploaderTest {
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()
readUntilRN() readUntilRN()
readUntilRN()
byte [] data = new byte[length] byte [] data = new byte[length]
DataInputStream dis = new DataInputStream(is) DataInputStream dis = new DataInputStream(is)

View File

@@ -1,5 +1,5 @@
group = com.muwire group = com.muwire
version = 0.4.11 version = 0.4.15
groovyVersion = 2.4.15 groovyVersion = 2.4.15
slf4jVersion = 1.7.25 slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4 spockVersion = 1.1-groovy-2.4

View File

@@ -44,9 +44,9 @@ mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
apply from: 'gradle/publishing.gradle' apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle' // apply from: 'gradle/code-coverage.gradle'
apply from: 'gradle/code-quality.gradle' // apply from: 'gradle/code-quality.gradle'
apply from: 'gradle/integration-test.gradle' // apply from: 'gradle/integration-test.gradle'
// apply from: 'gradle/package.gradle' // apply from: 'gradle/package.gradle'
apply from: 'gradle/docs.gradle' apply from: 'gradle/docs.gradle'
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
@@ -119,6 +119,7 @@ if (hasProperty('debugRun') && ((project.debugRun as boolean))) {
} }
} }
/*
task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) { task jacocoRootMerge(type: org.gradle.testing.jacoco.tasks.JacocoMerge, dependsOn: [test, jacocoTestReport, jacocoIntegrationTestReport]) {
executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData) executionData = files(jacocoTestReport.executionData, jacocoIntegrationTestReport.executionData)
destinationFile = file("${buildDir}/jacoco/root.exec") destinationFile = file("${buildDir}/jacoco/root.exec")
@@ -138,4 +139,5 @@ task jacocoRootReport(dependsOn: jacocoRootMerge, type: JacocoReport) {
xml.destination = file("${buildDir}/reports/jacoco/root/root.xml") xml.destination = file("${buildDir}/reports/jacoco/root/root.xml")
} }
} }
*/

View File

@@ -68,6 +68,16 @@ class ContentPanelController {
model.refresh() model.refresh()
} }
@ControllerAction
void clearHits() {
int selectedRule = view.getSelectedRule()
if (selectedRule < 0)
return
Matcher matcher = model.rules[selectedRule]
matcher.matches.clear()
model.refresh()
}
@ControllerAction @ControllerAction
void trust() { void trust() {
int selectedHit = view.getSelectedHit() int selectedHit = view.getSelectedHit()

View File

@@ -13,10 +13,10 @@ import javax.annotation.Nonnull
import javax.inject.Inject import javax.inject.Inject
import javax.swing.JTable import javax.swing.JTable
import com.muwire.core.Constants
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
import com.muwire.core.download.Downloader import com.muwire.core.download.Downloader
import com.muwire.core.download.DownloadStartedEvent import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent import com.muwire.core.download.UIDownloadCancelledEvent
@@ -80,13 +80,14 @@ class MainFrameController {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true) searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
} else { } else {
// this can be improved a lot // this can be improved a lot
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ") def replaced = search.toLowerCase().trim().replaceAll(SplitPattern.SPLIT_PATTERN, " ")
def terms = replaced.split(" ") def terms = replaced.split(" ")
def nonEmpty = [] def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it } terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true) searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
} }
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, boolean firstHop = core.muOptions.allowUntrusted || core.muOptions.searchExtraHop
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : firstHop,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me)) originator : core.me))
} }
@@ -269,10 +270,12 @@ class MainFrameController {
} }
void unshareSelectedFile() { void unshareSelectedFile() {
SharedFile sf = view.selectedSharedFile() def sf = view.selectedSharedFiles()
if (sf == null) if (sf == null)
return return
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf)) sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
} }
void stopWatchingDirectory() { void stopWatchingDirectory() {

View File

@@ -96,6 +96,10 @@ class OptionsController {
model.onlyTrusted = onlyTrusted model.onlyTrusted = onlyTrusted
settings.setAllowUntrusted(!onlyTrusted) settings.setAllowUntrusted(!onlyTrusted)
boolean searchExtraHop = view.searchExtraHopCheckbox.model.isSelected()
model.searchExtraHop = searchExtraHop
settings.searchExtraHop = searchExtraHop
boolean trustLists = view.allowTrustListsCheckbox.model.isSelected() boolean trustLists = view.allowTrustListsCheckbox.model.isSelected()
model.trustLists = trustLists model.trustLists = trustLists
settings.allowTrustLists = trustLists settings.allowTrustLists = trustLists

View File

@@ -8,63 +8,81 @@ import javax.annotation.Nonnull
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.download.UIDownloadEvent import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustLevel import com.muwire.core.trust.TrustLevel
@ArtifactProviderFor(GriffonController) @ArtifactProviderFor(GriffonController)
class SearchTabController { class SearchTabController {
@MVCMember @Nonnull @MVCMember @Nonnull
SearchTabModel model SearchTabModel model
@MVCMember @Nonnull @MVCMember @Nonnull
SearchTabView view SearchTabView view
Core core Core core
private def selectedResult() { private def selectedResults() {
int row = view.resultsTable.getSelectedRow() int[] rows = view.resultsTable.getSelectedRows()
if (row == -1) if (rows.length == 0)
return null return null
def sortEvt = view.lastSortEvent def sortEvt = view.lastSortEvent
if (sortEvt != null) { if (sortEvt != null) {
row = view.resultsTable.rowSorter.convertRowIndexToModel(row) for (int i = 0; i < rows.length; i++) {
rows[i] = view.resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
List<UIResultEvent> results = new ArrayList<>()
rows.each { results.add(model.results[it]) }
results
} }
model.results[row]
}
@ControllerAction
void download() {
def result = selectedResult()
if (result == null)
return
if (!mvcGroup.parentGroup.model.canDownload(result.infohash)) @ControllerAction
return void download() {
def results = selectedResults()
if (results == null)
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) results.removeAll {
!mvcGroup.parentGroup.model.canDownload(it.infohash)
}
def resultsBucket = model.hashBucket[result.infohash] results.each { result ->
def sources = model.sourcesBucket[result.infohash] def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file)) def resultsBucket = model.hashBucket[result.infohash]
mvcGroup.parentGroup.view.showDownloadsWindow.call() def sources = model.sourcesBucket[result.infohash]
}
@ControllerAction core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources,
void trust() { target : file, sequential : view.sequentialDownloadCheckbox.model.isSelected()))
int row = view.selectedSenderRow() }
if (row < 0) mvcGroup.parentGroup.view.showDownloadsWindow.call()
return }
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
}
@ControllerAction @ControllerAction
void distrust() { void trust() {
int row = view.selectedSenderRow() int row = view.selectedSenderRow()
if (row < 0) if (row < 0)
return return
def sender = model.senders[row] def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED)) core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.TRUSTED))
} }
}
@ControllerAction
void distrust() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void neutral() {
int row = view.selectedSenderRow()
if (row < 0)
return
def sender = model.senders[row]
core.eventBus.publish( new TrustEvent(persona : sender, level : TrustLevel.NEUTRAL))
}
}

View File

@@ -43,7 +43,7 @@ class Initialize extends AbstractLifecycleHandler {
application.context.put("muwire-home", home.getAbsolutePath()) application.context.put("muwire-home", home.getAbsolutePath())
System.getProperties().setProperty("awt.useSystemAAFontSettings", "true") System.getProperties().setProperty("awt.useSystemAAFontSettings", "gasp")
def guiPropsFile = new File(home, "gui.properties") def guiPropsFile = new File(home, "gui.properties")
UISettings uiSettings UISettings uiSettings
@@ -86,6 +86,8 @@ class Initialize extends AbstractLifecycleHandler {
} }
} else { } else {
LookAndFeel chosen = lookAndFeel('system', 'gtk') LookAndFeel chosen = lookAndFeel('system', 'gtk')
if (chosen == null)
chosen = lookAndFeel('metal')
uiSettings.lnf = chosen.getID() uiSettings.lnf = chosen.getID()
log.info("ended up applying $chosen.name") log.info("ended up applying $chosen.name")
} }

View File

@@ -40,11 +40,15 @@ class ContentPanelModel {
} }
void refresh() { void refresh() {
int selectedRule = view.getSelectedRule()
rules.clear() rules.clear()
rules.addAll(contentManager.matchers) rules.addAll(contentManager.matchers)
hits.clear() hits.clear()
view.rulesTable.model.fireTableDataChanged() view.rulesTable.model.fireTableDataChanged()
view.hitsTable.model.fireTableDataChanged() view.hitsTable.model.fireTableDataChanged()
if (selectedRule >= 0) {
view.rulesTable.selectionModel.setSelectionInterval(selectedRule,selectedRule)
}
} }
void onContentControlEvent(ContentControlEvent e) { void onContentControlEvent(ContentControlEvent e) {

View File

@@ -191,7 +191,7 @@ class MainFrameModel {
return return
int retryInterval = core.muOptions.downloadRetryInterval int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) { if (retryInterval > 0) {
retryInterval *= 60000 retryInterval *= 1000
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) { if (now - lastRetryTime > retryInterval) {
lastRetryTime = now lastRetryTime = now
@@ -207,7 +207,7 @@ class MainFrameModel {
} }
} }
}, 60000, 60000) }, 1000, 1000)
runInsideUIAsync { runInsideUIAsync {
trusted.addAll(core.trustService.good.values()) trusted.addAll(core.trustService.good.values())

View File

@@ -38,6 +38,7 @@ class OptionsModel {
// trust options // trust options
@Observable boolean onlyTrusted @Observable boolean onlyTrusted
@Observable boolean searchExtraHop
@Observable boolean trustLists @Observable boolean trustLists
@Observable String trustListInterval @Observable String trustListInterval
@@ -73,6 +74,7 @@ class OptionsModel {
} }
onlyTrusted = !settings.allowUntrusted() onlyTrusted = !settings.allowUntrusted()
searchExtraHop = settings.searchExtraHop
trustLists = settings.allowTrustLists trustLists = settings.allowTrustLists
trustListInterval = String.valueOf(settings.trustListInterval) trustListInterval = String.valueOf(settings.trustListInterval)
} }

View File

@@ -84,6 +84,7 @@ class ContentPanelView {
} }
panel (constraints : BorderLayout.SOUTH) { panel (constraints : BorderLayout.SOUTH) {
button(text : "Refresh", refreshAction) button(text : "Refresh", refreshAction)
button(text : "Clear Hits", clearHitsAction)
button(text : "Trust", enabled : bind {model.trustButtonsEnabled}, trustAction) button(text : "Trust", enabled : bind {model.trustButtonsEnabled}, trustAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
} }

View File

@@ -23,6 +23,7 @@ import javax.swing.table.DefaultTableCellRenderer
import com.muwire.core.Constants import com.muwire.core.Constants
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.download.Downloader import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent import com.muwire.core.files.FileSharedEvent
import com.muwire.core.trust.RemoteTrustList import com.muwire.core.trust.RemoteTrustList
@@ -615,22 +616,36 @@ class MainFrameView {
menu.show(event.getComponent(), event.getX(), event.getY()) menu.show(event.getComponent(), event.getX(), event.getY())
} }
def selectedSharedFile() { def selectedSharedFiles() {
def sharedFilesTable = builder.getVariable("shared-files-table") def sharedFilesTable = builder.getVariable("shared-files-table")
int selected = sharedFilesTable.getSelectedRow() int[] selected = sharedFilesTable.getSelectedRows()
if (selected < 0) if (selected.length == 0)
return null return null
if (lastSharedSortEvent != null) List<SharedFile> rv = new ArrayList<>()
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected) if (lastSharedSortEvent != null) {
model.shared[selected] for (int i = 0; i < selected.length; i ++) {
selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i])
}
}
selected.each {
rv.add(model.shared[it])
}
rv
} }
def copyHashToClipboard() { def copyHashToClipboard() {
def selected = selectedSharedFile() def selectedFiles = selectedSharedFiles()
if (selected == null) if (selectedFiles == null)
return return
String root = Base64.encode(selected.infoHash.getRoot()) String roots = ""
StringSelection selection = new StringSelection(root) for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
SharedFile selected = iterator.next()
String root = Base64.encode(selected.infoHash.getRoot())
roots += root
if (iterator.hasNext())
roots += "\n"
}
StringSelection selection = new StringSelection(roots)
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard() def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
clipboard.setContents(selection, null) clipboard.setContents(selection, null)
} }
@@ -772,9 +787,12 @@ class MainFrameView {
chooser.setFileHidingEnabled(false) chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select file to share") chooser.setDialogTitle("Select file to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY) chooser.setFileSelectionMode(JFileChooser.FILES_ONLY)
chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) { if (rv == JFileChooser.APPROVE_OPTION) {
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile())) chooser.getSelectedFiles().each {
model.core.eventBus.publish(new FileSharedEvent(file : it))
}
} }
} }
@@ -783,14 +801,16 @@ class MainFrameView {
chooser.setFileHidingEnabled(false) chooser.setFileHidingEnabled(false)
chooser.setDialogTitle("Select directory to watch") chooser.setDialogTitle("Select directory to watch")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY) chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
chooser.setMultiSelectionEnabled(true)
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) { if (rv == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile() chooser.getSelectedFiles().each { f ->
model.watched << f.getAbsolutePath() model.watched << f.getAbsolutePath()
application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath() application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath()
mvcGroup.controller.saveMuWireSettings() mvcGroup.controller.saveMuWireSettings()
builder.getVariable("watched-directories-table").model.fireTableDataChanged() builder.getVariable("watched-directories-table").model.fireTableDataChanged()
model.core.eventBus.publish(new FileSharedEvent(file : f)) model.core.eventBus.publish(new FileSharedEvent(file : f))
}
} }
} }

View File

@@ -55,6 +55,7 @@ class OptionsView {
def outBwField def outBwField
def allowUntrustedCheckbox def allowUntrustedCheckbox
def searchExtraHopCheckbox
def allowTrustListsCheckbox def allowTrustListsCheckbox
def trustListIntervalField def trustListIntervalField
@@ -70,7 +71,7 @@ class OptionsView {
gridBagLayout() gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0)) label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0)) retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0)) label(text : "seconds", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1)) label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1)) updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
@@ -138,11 +139,13 @@ class OptionsView {
gridBagLayout() gridBagLayout()
label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0)) label(text : "Allow only trusted connections", constraints : gbc(gridx: 0, gridy : 0))
allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0)) allowUntrustedCheckbox = checkBox(selected : bind {model.onlyTrusted}, constraints : gbc(gridx: 1, gridy : 0))
label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 1)) label(text : "Search extra hop", constraints : gbc(gridx:0, gridy:1))
allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 1)) searchExtraHopCheckbox = checkBox(selected : bind {model.searchExtraHop}, constraints : gbc(gridx: 1, gridy : 1))
label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:2)) label(text : "Allow others to view my trust list", constraints : gbc(gridx: 0, gridy : 2))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:2)) allowTrustListsCheckbox = checkBox(selected : bind {model.trustLists}, constraints : gbc(gridx: 1, gridy : 2))
label(text : "hours", constraints : gbc(gridx: 2, gridy:2)) label(text : "Update trust lists every ", constraints : gbc(gridx:0, gridy:3))
trustListIntervalField = textField(text : bind {model.trustListInterval}, constraints:gbc(gridx:1, gridy:3))
label(text : "hours", constraints : gbc(gridx: 2, gridy:3))
} }

View File

@@ -22,6 +22,8 @@ import com.muwire.core.util.DataUtil
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Color import java.awt.Color
import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
@@ -43,11 +45,13 @@ class SearchTabView {
def lastSendersSortEvent def lastSendersSortEvent
def resultsTable def resultsTable
def lastSortEvent def lastSortEvent
def sequentialDownloadCheckbox
void initUI() { void initUI() {
builder.with { builder.with {
def resultsTable def resultsTable
def sendersTable def sendersTable
def sequentialDownloadCheckbox
def pane = panel { def pane = panel {
gridLayout(rows :1, cols : 1) gridLayout(rows :1, cols : 1)
splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) { splitPane(orientation: JSplitPane.VERTICAL_SPLIT, continuousLayout : true, dividerLocation: 300 ) {
@@ -66,6 +70,7 @@ class SearchTabView {
} }
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction) button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction)
button(text : "Neutral", enabled: bind {model.trustButtonsEnabled}, neutralAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction)
} }
} }
@@ -82,7 +87,17 @@ class SearchTabView {
} }
} }
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction) gridLayout(rows: 1, cols: 3)
panel()
panel {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction)
}
panel {
gridBagLayout()
panel (constraints : gbc(gridx : 0, gridy : 0, weightx : 100))
sequentialDownloadCheckbox = checkBox(constraints : gbc(gridx : 1, gridy: 0, weightx : 0),selected : false, enabled : bind {model.downloadActionEnabled})
label(constraints: gbc(gridx: 2, gridy: 0, weightx : 0),text : "Download sequentially")
}
} }
} }
} }
@@ -94,18 +109,26 @@ class SearchTabView {
this.resultsTable = resultsTable this.resultsTable = resultsTable
this.sendersTable = sendersTable this.sendersTable = sendersTable
this.sequentialDownloadCheckbox = sequentialDownloadCheckbox
def selectionModel = resultsTable.getSelectionModel() def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
selectionModel.addListSelectionListener( { selectionModel.addListSelectionListener( {
int row = resultsTable.getSelectedRow() int[] rows = resultsTable.getSelectedRows()
if (row < 0) { if (rows.length == 0) {
model.downloadActionEnabled = false model.downloadActionEnabled = false
return return
} }
if (lastSortEvent != null) if (lastSortEvent != null) {
row = resultsTable.rowSorter.convertRowIndexToModel(row) for (int i = 0; i < rows.length; i ++) {
model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash) rows[i] = resultsTable.rowSorter.convertRowIndexToModel(rows[i])
}
}
boolean downloadActionEnabled = true
rows.each {
downloadActionEnabled &= mvcGroup.parentGroup.model.canDownload(model.results[it].infohash)
}
model.downloadActionEnabled = downloadActionEnabled
}) })
} }
} }
@@ -190,21 +213,28 @@ class SearchTabView {
def showPopupMenu(MouseEvent e) { def showPopupMenu(MouseEvent e) {
JPopupMenu menu = new JPopupMenu() JPopupMenu menu = new JPopupMenu()
boolean showMenu = false
if (model.downloadActionEnabled) { if (model.downloadActionEnabled) {
JMenuItem download = new JMenuItem("Download") JMenuItem download = new JMenuItem("Download")
download.addActionListener({mvcGroup.controller.download()}) download.addActionListener({mvcGroup.controller.download()})
menu.add(download) menu.add(download)
showMenu = true
} }
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard") if (resultsTable.getSelectedRows().length == 1) {
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()}) JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
menu.add(copyHashToClipboard) copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
menu.show(e.getComponent(), e.getX(), e.getY()) menu.add(copyHashToClipboard)
showMenu = true
}
if (showMenu)
menu.show(e.getComponent(), e.getX(), e.getY())
} }
def copyHashToClipboard() { def copyHashToClipboard() {
int selected = resultsTable.getSelectedRow() int[] selectedRows = resultsTable.getSelectedRows()
if (selected < 0) if (selectedRows.length != 1)
return return
int selected = selectedRows[0]
if (lastSortEvent != null) if (lastSortEvent != null)
selected = resultsTable.rowSorter.convertRowIndexToModel(selected) selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
String hash = Base64.encode(model.results[selected].infohash.getRoot()) String hash = Base64.encode(model.results[selected].infohash.getRoot())

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(ContentPanelController)
class ContentPanelControllerTest {
private ContentPanelController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(EventListController)
class EventListControllerTest {
private EventListController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(I2PStatusController)
class I2PStatusControllerTest {
private I2PStatusController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(MainFrameController)
class MainFrameControllerTest {
private MainFrameController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(MuWireStatusController)
class MuWireStatusControllerTest {
private MuWireStatusController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(OptionsController)
class OptionsControllerTest {
private OptionsController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(SearchTabController)
class SearchTabControllerTest {
private SearchTabController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,21 +0,0 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(TrustListController)
class TrustListControllerTest {
private TrustListController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@@ -1,3 +1,8 @@
apply plugin : 'application' apply plugin : 'application'
mainClassName = 'com.muwire.hostcache.HostCache' mainClassName = 'com.muwire.hostcache.HostCache'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties'] applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'
}

View File

@@ -9,6 +9,7 @@ import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.datagram.I2PDatagramDissector import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.crypto.SigType
@Log @Log
@@ -28,7 +29,7 @@ class UpdateServer {
def session def session
if (!keyFile.exists()) { if (!keyFile.exists()) {
def os = new FileOutputStream(keyFile); def os = new FileOutputStream(keyFile);
myDest = i2pClient.createDestination(os) myDest = i2pClient.createDestination(os, SigType.EdDSA_SHA512_Ed25519)
os.close() os.close()
log.info "No key.dat file was found, so creating a new destination." log.info "No key.dat file was found, so creating a new destination."
log.info "This is the destination you want to give out for your new UpdateServer" log.info "This is the destination you want to give out for your new UpdateServer"