Compare commits

..

32 Commits

Author SHA1 Message Date
Zlatin Balevsky
7bb5e5b632 Release 0.3.4 2019-06-20 21:07:50 +01:00
Zlatin Balevsky
b2e43f9765 update split pattern and add unit test 2019-06-20 21:06:39 +01:00
Zlatin Balevsky
2aa73c203a Release 0.3.3 2019-06-20 18:08:02 +01:00
Zlatin Balevsky
18d2b56563 fix indexing 2019-06-20 17:57:36 +01:00
Zlatin Balevsky
a455b4ad6e redirect exceptions in result sender to log 2019-06-20 17:22:59 +01:00
Zlatin Balevsky
761b683a81 Release 0.3.2 2019-06-20 16:04:46 +01:00
Zlatin Balevsky
1d41bcd825 prevent empty tokens in search index 2019-06-20 16:02:48 +01:00
Zlatin Balevsky
f1ac038b55 update split pattern 2019-06-20 15:47:00 +01:00
Zlatin Balevsky
396c636e42 prevent empty search terms 2019-06-20 15:29:27 +01:00
Zlatin Balevsky
e32c858e90 update README with quick FAQ 2019-06-20 14:18:37 +01:00
Zlatin Balevsky
821555f3f1 Release 0.3.1 2019-06-20 14:02:22 +01:00
Zlatin Balevsky
089ab4f0d9 do not retry downloads if core is shut(ting) down 2019-06-20 13:40:04 +01:00
Zlatin Balevsky
948b6292fe add shutdown hook to shutdown core on SIGTERM 2019-06-20 13:29:15 +01:00
Zlatin Balevsky
4e2a530a13 Release 0.3.0 2019-06-20 07:04:45 +01:00
Zlatin Balevsky
03646e2b90 Document download mesh 2019-06-20 01:19:15 +01:00
Zlatin Balevsky
3dce228bbb always clean 2019-06-19 22:42:05 +01:00
Zlatin Balevsky
15a49ad550 show git revision in title 2019-06-19 22:36:22 +01:00
Zlatin Balevsky
3d91c0f4c7 increase default tunnel count 2019-06-19 22:24:04 +01:00
Zlatin Balevsky
2825a8d9a4 Release 0.2.10 2019-06-19 17:18:30 +01:00
Zlatin Balevsky
8dcce9bda6 Merge branch 'connection-logic' 2019-06-19 17:16:13 +01:00
Zlatin Balevsky
d8d3e2cd58 update tests 2019-06-19 15:54:35 +01:00
Zlatin Balevsky
51d5dbe47e Prevent rare exception on changing trust when result tabs are open 2019-06-19 12:23:18 +01:00
Zlatin Balevsky
84cee0aa43 retry failed hosts after one hour 2019-06-19 08:35:31 +01:00
Zlatin Balevsky
162844787f explicitly set java versions 2019-06-19 02:11:00 +01:00
Zlatin Balevsky
d8a2b59055 tool to print out contents of files.json 2019-06-18 22:08:33 +01:00
Zlatin Balevsky
67a0939de4 Release 0.2.9 2019-06-18 20:15:53 +01:00
Zlatin Balevsky
37ca922a2c reduce default retry interval 2019-06-18 20:07:20 +01:00
Zlatin Balevsky
1d6781819b ignore CWSE if shutting down 2019-06-18 19:44:22 +01:00
Zlatin Balevsky
64d45da94a show version on title 2019-06-18 18:57:44 +01:00
Zlatin Balevsky
59c84d8a5e Release 0.2.8 2019-06-18 17:48:07 +01:00
Zlatin Balevsky
8b55021a4b fix 2019-06-18 17:23:18 +01:00
Zlatin Balevsky
8bd3ebfaf5 timestamp entries 2019-06-18 17:17:03 +01:00
28 changed files with 192 additions and 86 deletions

View File

@@ -11,12 +11,12 @@ The current stable release - 0.2.5 is avaiable for download at http://muwire.com
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
``` ```
./gradlew assemble ./gradlew clean assemble
``` ```
If you want to run the unit tests, type If you want to run the unit tests, type
``` ```
./gradlew build ./gradlew clean build
``` ```
Some of the UI tests will fail because they haven't been written yet :-/ Some of the UI tests will fail because they haven't been written yet :-/
@@ -31,3 +31,19 @@ The first time you run MuWire it will ask you to select a nickname. This nickna
### Known bugs and limitations ### Known bugs and limitations
* Many UI features you would expect are not there yet * Many UI features you would expect are not there yet
### Quick FAQ
* why is MuWire slow ?
- too few sources you're downloading from
- you can increse the number of tunnels by using more tunnels via Options->I2P Inbound/Outbound Quantity
the default is 4 and you could raise it and even can go up as high as 16 ( Caution !!!!)
* my search is not returning (enough) results !
- search is keyword or hash based
- keywords and hash(es) are NOT regexed or wildcarded so they have to be complete
so searching for 'musi' will not return results with 'music' - you have to search for 'music'
- ALL keywords have to match
- only use <SPACE> for keyword separation

View File

@@ -35,7 +35,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.2.7") core = new Core(props, home, "0.3.4")
} 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"
@@ -73,7 +73,7 @@ class Cli {
Timer timer = new Timer("status-printer", true) Timer timer = new Timer("status-printer", true)
timer.schedule({ timer.schedule({
println "Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared" println String.valueOf(new Date()) + " Connections $connectionsListener.connections Uploads $uploadsListener.uploads Shared $sharedListener.shared"
} as TimerTask, 60000, 60000) } as TimerTask, 60000, 60000)
def latch = new CountDownLatch(1) def latch = new CountDownLatch(1)
@@ -119,11 +119,11 @@ class Cli {
volatile int uploads volatile int uploads
public void onUploadEvent(UploadEvent e) { public void onUploadEvent(UploadEvent e) {
uploads++ uploads++
println "Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}" println String.valueOf(new Date()) + " Starting upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
} }
public void onUploadFinishedEvent(UploadFinishedEvent e) { public void onUploadFinishedEvent(UploadFinishedEvent e) {
uploads-- uploads--
println "Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}" println String.valueOf(new Date()) + " Finished upload of ${e.uploader.file.getName()} to ${e.uploader.request.downloader.getHumanReadableName()}"
} }
} }

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core Core core
try { try {
core = new Core(props, home, "0.2.7") core = new Core(props, home, "0.3.4")
} 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

@@ -0,0 +1,23 @@
package com.muwire.cli
import com.muwire.core.util.DataUtil
import groovy.json.JsonSlurper
import net.i2p.data.Base64
class FileList {
public static void main(String [] args) {
if (args.length < 1) {
println "pass files.json as argument"
System.exit(1)
}
def slurper = new JsonSlurper()
File filesJson = new File(args[0])
filesJson.eachLine {
def json = slurper.parseText(it)
String name = DataUtil.readi18nString(Base64.decode(json.file))
println "$name,$json.length,$json.pieceSize,$json.infoHash"
}
}
}

View File

@@ -11,5 +11,5 @@ class Constants {
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]" public static final String SPLIT_PATTERN = "[\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|]"
} }

View File

@@ -1,6 +1,7 @@
package com.muwire.core package com.muwire.core
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.connection.ConnectionAcceptor import com.muwire.core.connection.ConnectionAcceptor
import com.muwire.core.connection.ConnectionEstablisher import com.muwire.core.connection.ConnectionEstablisher
@@ -73,6 +74,8 @@ public class Core {
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final DirectoryWatcher directoryWatcher private final DirectoryWatcher directoryWatcher
final FileManager fileManager final FileManager fileManager
final AtomicBoolean shutdown = new AtomicBoolean()
public Core(MuWireSettings props, File home, String myVersion) { public Core(MuWireSettings props, File home, String myVersion) {
this.home = home this.home = home
@@ -104,9 +107,9 @@ public class Core {
i2pOptions["inbound.nickname"] = "MuWire" i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire" i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3" i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "2" i2pOptions["inbound.quantity"] = "4"
i2pOptions["outbound.length"] = "3" i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "2" i2pOptions["outbound.quantity"] = "4"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1" i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654" i2pOptions["i2cp.tcp.port"] = "7654"
} }
@@ -241,6 +244,10 @@ public class Core {
} }
public void shutdown() { public void shutdown() {
if (!shutdown.compareAndSet(false, true)) {
log.info("already shutting down")
return
}
log.info("shutting down download manageer") log.info("shutting down download manageer")
downloadManager.shutdown() downloadManager.shutdown()
log.info("shutting down connection acceeptor") log.info("shutting down connection acceeptor")
@@ -277,7 +284,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.2.7") Core core = new Core(props, home, "0.3.4")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -30,7 +30,7 @@ class MuWireSettings {
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","15")) downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","5"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36")) updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true")) shareDownloadedFiles = Boolean.parseBoolean(props.getProperty("shareDownloadedFiles","true"))

View File

@@ -5,6 +5,8 @@ import java.nio.file.FileSystems
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import static java.nio.file.StandardWatchEventKinds.* import static java.nio.file.StandardWatchEventKinds.*
import java.nio.file.ClosedWatchServiceException
import java.nio.file.WatchEvent import java.nio.file.WatchEvent
import java.nio.file.WatchKey import java.nio.file.WatchKey
import java.nio.file.WatchService import java.nio.file.WatchService
@@ -79,7 +81,7 @@ class DirectoryWatcher {
} }
key.reset() key.reset()
} }
} catch (InterruptedException e) { } catch (InterruptedException|ClosedWatchServiceException e) {
if (!shutdown) if (!shutdown)
throw e throw e
} }

View File

@@ -5,9 +5,11 @@ import net.i2p.data.Destination
class Host { class Host {
private static final int MAX_FAILURES = 3 private static final int MAX_FAILURES = 3
private static final int CLEAR_INTERVAL = 60 * 60 * 1000
final Destination destination final Destination destination
int failures,successes int failures,successes
long lastAttempt
public Host(Destination destination) { public Host(Destination destination) {
this.destination = destination this.destination = destination
@@ -16,11 +18,13 @@ class Host {
synchronized void onConnect() { synchronized void onConnect() {
failures = 0 failures = 0
successes++ successes++
lastAttempt = System.currentTimeMillis()
} }
synchronized void onFailure() { synchronized void onFailure() {
failures++ failures++
successes = 0 successes = 0
lastAttempt = System.currentTimeMillis()
} }
synchronized boolean isFailed() { synchronized boolean isFailed() {
@@ -34,4 +38,8 @@ class Host {
synchronized void clearFailures() { synchronized void clearFailures() {
failures = 0 failures = 0
} }
synchronized void canTryAgain() {
System.currentTimeMillis() - lastAttempt > CLEAR_INTERVAL
}
} }

View File

@@ -109,6 +109,8 @@ class HostCache extends Service {
Host host = new Host(dest) Host host = new Host(dest)
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)
host.lastAttempt = entry.lastAttempt
if (allowHost(host)) if (allowHost(host))
hosts.put(dest, host) hosts.put(dest, host)
} }
@@ -118,7 +120,7 @@ class HostCache extends Service {
} }
private boolean allowHost(Host host) { private boolean allowHost(Host host) {
if (host.isFailed()) if (host.isFailed() && !host.canTryAgain())
return false return false
if (host.destination == myself) if (host.destination == myself)
return false return false
@@ -143,6 +145,7 @@ class HostCache extends Service {
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
def json = JsonOutput.toJson(map) def json = JsonOutput.toJson(map)
writer.println json writer.println json
} }

View File

@@ -11,6 +11,7 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import java.util.stream.Collectors import java.util.stream.Collectors
import com.muwire.core.DownloadedFile import com.muwire.core.DownloadedFile
@@ -83,50 +84,54 @@ class ResultsSender {
@Override @Override
public void run() { public void run() {
byte [] tmp = new byte[InfoHash.SIZE]
JsonOutput jsonOutput = new JsonOutput()
Endpoint endpoint = null;
try { try {
endpoint = connector.connect(target) byte [] tmp = new byte[InfoHash.SIZE]
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream()) JsonOutput jsonOutput = new JsonOutput()
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII)) Endpoint endpoint = null;
me.write(os) try {
os.writeShort((short)results.length) endpoint = connector.connect(target)
results.each { DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8) os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
def baos = new ByteArrayOutputStream() me.write(os)
def daos = new DataOutputStream(baos) os.writeShort((short)results.length)
daos.writeShort((short) name.length) results.each {
daos.write(name) byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
daos.flush() def baos = new ByteArrayOutputStream()
String encodedName = Base64.encode(baos.toByteArray()) def daos = new DataOutputStream(baos)
def obj = [:] daos.writeShort((short) name.length)
obj.type = "Result" daos.write(name)
obj.version = oobInfohash ? 2 : 1 daos.flush()
obj.name = encodedName String encodedName = Base64.encode(baos.toByteArray())
obj.infohash = Base64.encode(it.getInfoHash().getRoot()) def obj = [:]
obj.size = it.getFile().length() obj.type = "Result"
obj.pieceSize = it.getPieceSize() obj.version = oobInfohash ? 2 : 1
if (!oobInfohash) { obj.name = encodedName
byte [] hashList = it.getInfoHash().getHashList() obj.infohash = Base64.encode(it.getInfoHash().getRoot())
def hashListB64 = [] obj.size = it.getFile().length()
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) { obj.pieceSize = it.getPieceSize()
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE) if (!oobInfohash) {
hashListB64 << Base64.encode(tmp) byte [] hashList = it.getInfoHash().getHashList()
def hashListB64 = []
for (int i = 0; i < hashList.length / InfoHash.SIZE; i++) {
System.arraycopy(hashList, InfoHash.SIZE * i, tmp, 0, InfoHash.SIZE)
hashListB64 << Base64.encode(tmp)
}
obj.hashList = hashListB64
} }
obj.hashList = hashListB64
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
} }
os.flush()
if (it instanceof DownloadedFile) } finally {
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet()) endpoint?.close()
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII))
} }
os.flush() } catch (Exception e) {
} finally { log.log(Level.WARNING, "problem sending results",e)
endpoint?.close()
} }
} }
} }

View File

@@ -33,7 +33,10 @@ class SearchIndex {
private static String[] split(String source) { private static String[] split(String source) {
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase() source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source.split(" ") String [] split = source.split(" ")
def rv = []
split.each { if (it.length() > 0) rv << it }
rv.toArray(new String[0])
} }
String[] search(List<String> terms) { String[] search(List<String> terms) {

View File

@@ -15,7 +15,7 @@ class DownloadSessionTest {
private File source, target private File source, target
private InfoHash infoHash private InfoHash infoHash
private Endpoint endpoint private Endpoint endpoint
private Pieces pieces, claimed private Pieces pieces
private String rootBase64 private String rootBase64
private DownloadSession session private DownloadSession session
@@ -48,8 +48,7 @@ class DownloadSessionTest {
else else
nPieces = size / pieceSize + 1 nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces) pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces) claimedPieces.each {pieces.claimed.set(it)}
claimedPieces.each {claimed.markDownloaded(it)}
fromDownloader = new PipedInputStream() fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream() fromUploader = new PipedInputStream()
@@ -57,7 +56,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader) toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null) endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size) session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable) downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true) downloadThread.setDaemon(true)
downloadThread.start() downloadThread.start()
@@ -154,7 +153,7 @@ class DownloadSessionTest {
int pieceSize = FileHasher.getPieceSize(1) int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10 int size = (1 << pieceSize) * 10
initSession(size, [1,2,3,4,5,6,7,8,9]) initSession(size, [1,2,3,4,5,6,7,8,9])
assert !claimed.isMarked(0) assert !pieces.claimed.get(0)
assert "GET $rootBase64" == readTillRN(fromDownloader) assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader) String range = readTillRN(fromDownloader)
@@ -162,7 +161,7 @@ class DownloadSessionTest {
int start = Integer.parseInt(matcher[0][1]) int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2]) int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0) assert pieces.claimed.get(0)
assert start == 0 && end == (1 << pieceSize) - 1 assert start == 0 && end == (1 << pieceSize) - 1
} }
} }

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.getRandomPiece() == 0 assert pieces.claim() == 0
pieces.markDownloaded(0) pieces.markDownloaded(0)
assert pieces.isComplete() assert pieces.isComplete()
} }
@@ -25,11 +25,11 @@ 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.getRandomPiece() int piece = pieces.claim()
assert piece == 0 || piece == 1 assert piece == 0 || piece == 1
pieces.markDownloaded(piece) pieces.markDownloaded(piece)
assert !pieces.isComplete() assert !pieces.isComplete()
int piece2 = pieces.getRandomPiece() int piece2 = pieces.claim()
assert piece != piece2 assert piece != piece2
pieces.markDownloaded(piece2) pieces.markDownloaded(piece2)
assert pieces.isComplete() assert pieces.isComplete()

View File

@@ -26,7 +26,7 @@ class FileHasherTest extends GroovyTestCase {
void testPieceSize() { void testPieceSize() {
assert 17 == FileHasher.getPieceSize(1000000) assert 17 == FileHasher.getPieceSize(1000000)
assert 17 == FileHasher.getPieceSize(100000000) assert 17 == FileHasher.getPieceSize(100000000)
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE) assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, { shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE) FileHasher.getPieceSize(Long.MAX_VALUE)
} }

View File

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

View File

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

View File

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

View File

@@ -83,4 +83,11 @@ class SearchIndexTest {
assert found.size() == 1 assert found.size() == 1
assert found.contains("b c.d") assert found.contains("b c.d")
} }
@Test
void testDuplicateTerm() {
initIndex(["MuWire-0.3.3.jar"])
def found = index.search(["muwire", "0", "3", "jar"])
assert found.size() == 1
}
} }

View File

@@ -9,18 +9,18 @@ import com.muwire.core.InfoHash
class RequestParsingTest { class RequestParsingTest {
Request request ContentRequest request
private void fromString(String requestString) { private void fromString(String requestString) {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII)) def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
request = Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is) request = Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
} }
private static void failed(String requestString) { private static void failed(String requestString) {
try { try {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII)) def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is) Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
assert false assert false
} catch (IOException expected) {} } catch (IOException expected) {}
} }

View File

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

View File

@@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
### Mesh management ### Mesh management
Download mesh management is identical to Gnutella, except instead of ip addresses MuWire personas are used. [More information](http://rfc-gnutella.sourceforge.net/developer/tmp/download-mesh.html) Download mesh management is a simplified version of Gnutella's "Alternate Location" system. For more information see the "download-mesh" document.
### In-Network updates ### In-Network updates

15
doc/download-mesh.md Normal file
View File

@@ -0,0 +1,15 @@
# Download Mesh / Partial Sharing
MuWire uses a system similar to Gnutella's "Alternate Location" download mesh management system, however it is simplified to account for I2P's strengths and borrows a bit from BitTorrent's "Have" message.
### "X-Have" header
With every request a downloader makes it sends an "X-Have" header containing the Base64-encoded representation of a bitfield where bits set to 1 represent pieces of the file that the downloader already has. To make partial file sharing possible, if the uploader does not have the complete file it also sends this header in every response. If the header is missing it is assumed the uploader has the complete file.
### "X-Alt" header
The uploader can recommend other uploaders to the downloader via the "X-Alt" header. The format of this header is a comma-separated list of Base64-encoded Personas that have previously reported having at least one piece of the file to the uploader via the "X-Have" header.
### Differences from Gnutella
Unlike Gnutella the uploader is the sole repository where possible sources of the file are tracked. There is no negative "X-Nalt" header to prevent attacking the download mesh by mass downvoting of sources.

View File

@@ -1,5 +1,8 @@
group = com.muwire group = com.muwire
version = 0.2.7 version = 0.3.4
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
sourceCompatibility=1.8
targetCompatibility=1.8

View File

@@ -67,7 +67,9 @@ class MainFrameController {
// 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(Constants.SPLIT_PATTERN, " ")
def terms = replaced.split(" ") def terms = replaced.split(" ")
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true) def nonEmpty = []
terms.each { if (it.length() > 0) nonEmpty << it }
searchEvent = new SearchEvent(searchTerms : nonEmpty, uuid : uuid, oobInfohash: true)
} }
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true, core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination, replyTo: core.me.destination, receivedOn: core.me.destination,

View File

@@ -98,6 +98,9 @@ class Ready extends AbstractLifecycleHandler {
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE) "Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0) System.exit(0)
} }
Runtime.getRuntime().addShutdownHook({
core.shutdown()
})
core.startServices() core.startServices()
application.context.put("muwire-settings", props) application.context.put("muwire-settings", props)
application.context.put("core",core) application.context.put("core",core)

View File

@@ -137,6 +137,8 @@ class MainFrameModel {
core.eventBus.register(FileUnsharedEvent.class, this) core.eventBus.register(FileUnsharedEvent.class, this)
timer.schedule({ timer.schedule({
if (core.shutdown.get())
return
int retryInterval = core.muOptions.downloadRetryInterval int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) { if (retryInterval > 0) {
retryInterval *= 60000 retryInterval *= 60000
@@ -282,8 +284,10 @@ class MainFrameModel {
updateTablePreservingSelection("trusted-table") updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table") updateTablePreservingSelection("distrusted-table")
results.values().each { results.values().each { MVCGroup group ->
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged() if (group.alive) {
group.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package com.muwire.gui package com.muwire.gui
import griffon.core.artifact.GriffonView import griffon.core.artifact.GriffonView
import griffon.core.env.Metadata
import griffon.inject.MVCMember import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor import griffon.metadata.ArtifactProviderFor
import net.i2p.data.Base64 import net.i2p.data.Base64
@@ -37,6 +38,7 @@ import java.awt.event.MouseEvent
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull import javax.annotation.Nonnull
import javax.inject.Inject
@ArtifactProviderFor(GriffonView) @ArtifactProviderFor(GriffonView)
class MainFrameView { class MainFrameView {
@@ -45,6 +47,8 @@ class MainFrameView {
@MVCMember @Nonnull @MVCMember @Nonnull
MainFrameModel model MainFrameModel model
@Inject Metadata metadata
def downloadsTable def downloadsTable
def lastDownloadSortEvent def lastDownloadSortEvent
def lastSharedSortEvent def lastSharedSortEvent
@@ -54,7 +58,8 @@ class MainFrameView {
builder.with { builder.with {
application(size : [1024,768], id: 'main-frame', application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null, locationRelativeTo : null,
title: application.configuration['application.title'], title: application.configuration['application.title'] + " " +
metadata["application.version"] + " revision " + metadata["build.revision"],
iconImage: imageIcon('/griffon-icon-48x48.png').image, iconImage: imageIcon('/griffon-icon-48x48.png').image,
iconImages: [imageIcon('/griffon-icon-48x48.png').image, iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image, imageIcon('/griffon-icon-32x32.png').image,