Compare commits
18 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 |
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
|
||||
|
||||
```
|
||||
./gradlew assemble
|
||||
./gradlew clean assemble
|
||||
```
|
||||
|
||||
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 :-/
|
||||
@ -31,3 +31,19 @@ The first time you run MuWire it will ask you to select a nickname. This nickna
|
||||
### Known bugs and limitations
|
||||
|
||||
* 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
|
||||
try {
|
||||
core = new Core(props, home, "0.2.10")
|
||||
core = new Core(props, home, "0.3.4")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -53,7 +53,7 @@ class CliDownloader {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.2.10")
|
||||
core = new Core(props, home, "0.3.4")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -11,5 +11,5 @@ class Constants {
|
||||
|
||||
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
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.muwire.core.connection.ConnectionAcceptor
|
||||
import com.muwire.core.connection.ConnectionEstablisher
|
||||
@ -73,6 +74,8 @@ public class Core {
|
||||
private final DownloadManager downloadManager
|
||||
private final DirectoryWatcher directoryWatcher
|
||||
final FileManager fileManager
|
||||
|
||||
final AtomicBoolean shutdown = new AtomicBoolean()
|
||||
|
||||
public Core(MuWireSettings props, File home, String myVersion) {
|
||||
this.home = home
|
||||
@ -104,9 +107,9 @@ public class Core {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
i2pOptions["inbound.length"] = "3"
|
||||
i2pOptions["inbound.quantity"] = "2"
|
||||
i2pOptions["inbound.quantity"] = "4"
|
||||
i2pOptions["outbound.length"] = "3"
|
||||
i2pOptions["outbound.quantity"] = "2"
|
||||
i2pOptions["outbound.quantity"] = "4"
|
||||
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||
}
|
||||
@ -241,6 +244,10 @@ public class Core {
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (!shutdown.compareAndSet(false, true)) {
|
||||
log.info("already shutting down")
|
||||
return
|
||||
}
|
||||
log.info("shutting down download manageer")
|
||||
downloadManager.shutdown()
|
||||
log.info("shutting down connection acceeptor")
|
||||
@ -277,7 +284,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.2.10")
|
||||
Core core = new Core(props, home, "0.3.4")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@ -11,6 +11,7 @@ import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.logging.Level
|
||||
import java.util.stream.Collectors
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
@ -83,50 +84,54 @@ class ResultsSender {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) name.length)
|
||||
daos.write(name)
|
||||
daos.flush()
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = oobInfohash ? 2 : 1
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||
obj.size = it.getFile().length()
|
||||
obj.pieceSize = it.getPieceSize()
|
||||
if (!oobInfohash) {
|
||||
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)
|
||||
byte [] tmp = new byte[InfoHash.SIZE]
|
||||
JsonOutput jsonOutput = new JsonOutput()
|
||||
Endpoint endpoint = null;
|
||||
try {
|
||||
endpoint = connector.connect(target)
|
||||
DataOutputStream os = new DataOutputStream(endpoint.getOutputStream())
|
||||
os.write("POST $uuid\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
me.write(os)
|
||||
os.writeShort((short)results.length)
|
||||
results.each {
|
||||
byte [] name = it.getFile().getName().getBytes(StandardCharsets.UTF_8)
|
||||
def baos = new ByteArrayOutputStream()
|
||||
def daos = new DataOutputStream(baos)
|
||||
daos.writeShort((short) name.length)
|
||||
daos.write(name)
|
||||
daos.flush()
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = oobInfohash ? 2 : 1
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||
obj.size = it.getFile().length()
|
||||
obj.pieceSize = it.getPieceSize()
|
||||
if (!oobInfohash) {
|
||||
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))
|
||||
}
|
||||
|
||||
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()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
}
|
||||
os.flush()
|
||||
} finally {
|
||||
endpoint?.close()
|
||||
} catch (Exception e) {
|
||||
log.log(Level.WARNING, "problem sending results",e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,10 @@ class SearchIndex {
|
||||
|
||||
private static String[] split(String source) {
|
||||
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) {
|
||||
|
@ -83,4 +83,11 @@ class SearchIndexTest {
|
||||
assert found.size() == 1
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
|
||||
|
||||
### 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
|
||||
|
||||
|
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,5 @@
|
||||
group = com.muwire
|
||||
version = 0.2.10
|
||||
version = 0.3.4
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
@ -67,7 +67,9 @@ class MainFrameController {
|
||||
// this can be improved a lot
|
||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||
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,
|
||||
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)
|
||||
System.exit(0)
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook({
|
||||
core.shutdown()
|
||||
})
|
||||
core.startServices()
|
||||
application.context.put("muwire-settings", props)
|
||||
application.context.put("core",core)
|
||||
|
@ -137,6 +137,8 @@ class MainFrameModel {
|
||||
core.eventBus.register(FileUnsharedEvent.class, this)
|
||||
|
||||
timer.schedule({
|
||||
if (core.shutdown.get())
|
||||
return
|
||||
int retryInterval = core.muOptions.downloadRetryInterval
|
||||
if (retryInterval > 0) {
|
||||
retryInterval *= 60000
|
||||
|
@ -58,7 +58,8 @@ class MainFrameView {
|
||||
builder.with {
|
||||
application(size : [1024,768], id: 'main-frame',
|
||||
locationRelativeTo : null,
|
||||
title: application.configuration['application.title'] + " " + metadata["application.version"],
|
||||
title: application.configuration['application.title'] + " " +
|
||||
metadata["application.version"] + " revision " + metadata["build.revision"],
|
||||
iconImage: imageIcon('/griffon-icon-48x48.png').image,
|
||||
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
|
||||
imageIcon('/griffon-icon-32x32.png').image,
|
||||
|
Reference in New Issue
Block a user