Compare commits
32 Commits
muwire-0.2
...
muwire-0.3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7bb5e5b632 | ||
![]() |
b2e43f9765 | ||
![]() |
2aa73c203a | ||
![]() |
18d2b56563 | ||
![]() |
a455b4ad6e | ||
![]() |
761b683a81 | ||
![]() |
1d41bcd825 | ||
![]() |
f1ac038b55 | ||
![]() |
396c636e42 | ||
![]() |
e32c858e90 | ||
![]() |
821555f3f1 | ||
![]() |
089ab4f0d9 | ||
![]() |
948b6292fe | ||
![]() |
4e2a530a13 | ||
![]() |
03646e2b90 | ||
![]() |
3dce228bbb | ||
![]() |
15a49ad550 | ||
![]() |
3d91c0f4c7 | ||
![]() |
2825a8d9a4 | ||
![]() |
8dcce9bda6 | ||
![]() |
d8d3e2cd58 | ||
![]() |
51d5dbe47e | ||
![]() |
84cee0aa43 | ||
![]() |
162844787f | ||
![]() |
d8a2b59055 | ||
![]() |
67a0939de4 | ||
![]() |
37ca922a2c | ||
![]() |
1d6781819b | ||
![]() |
64d45da94a | ||
![]() |
59c84d8a5e | ||
![]() |
8b55021a4b | ||
![]() |
8bd3ebfaf5 |
20
README.md
20
README.md
@@ -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
|
||||||
|
@@ -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()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
23
cli/src/main/groovy/com/muwire/cli/FileList.groovy
Normal file
23
cli/src/main/groovy/com/muwire/cli/FileList.groovy
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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 = "[\\+\\-,\\.:;\\(\\)=_/\\\\\\!\\\"\\\'\\\$%\\|]"
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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"))
|
||||||
|
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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) {}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
15
doc/download-mesh.md
Normal 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.
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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)
|
||||||
|
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user