Compare commits
44 Commits
muwire-0.1
...
muwire-0.1
Author | SHA1 | Date | |
---|---|---|---|
34d9165bd5 | |||
2e52dd5c49 | |||
2a315dd734 | |||
6b661b99c5 | |||
5dacd60bbb | |||
f8f7cfe836 | |||
0b4f261bc1 | |||
042d67d784 | |||
800df88f14 | |||
4d1eac50a0 | |||
c48df7f14b | |||
9d04148001 | |||
bb4d522572 | |||
8052501e52 | |||
66cc6d8ab7 | |||
a45e57f5ec | |||
7d8ca55d87 | |||
de22f3c6b9 | |||
3b0eb5678d | |||
5a1f32e40b | |||
ca3f2513e1 | |||
658d9cf5a8 | |||
e389090b7e | |||
04ceaba514 | |||
6a01d97a8d | |||
747663e1dc | |||
e426b3ccbd | |||
5172e19627 | |||
e826cfd8d5 | |||
51004f6fe9 | |||
08bb2b614d | |||
d0e5d0ce8a | |||
9e05802d1b | |||
fb4f56eec9 | |||
be2083d430 | |||
af6275d0a3 | |||
5269815329 | |||
bd21cf65ea | |||
dea592eb27 | |||
c81f963e0a | |||
dc6b1199f3 | |||
42621a2dfb | |||
a7125963a7 | |||
f39d7f4fa8 |
@ -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.
|
||||
|
||||
The first stable release - 0.1.0 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
The current stable release - 0.1.5 is avaiable for download at http://muwire.com. You can find technical documentation in the "doc" folder.
|
||||
|
||||
### Building
|
||||
|
||||
You need JDK 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
|
||||
@ -31,5 +31,4 @@ 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
|
||||
* Downloads in progress do not get remembered between restarts
|
||||
|
||||
* Sorting the results table sometimes causes the wrong result to be downloaded
|
||||
|
@ -34,7 +34,7 @@ class Cli {
|
||||
|
||||
Core core
|
||||
try {
|
||||
core = new Core(props, home, "0.1.3")
|
||||
core = new Core(props, home, "0.1.10")
|
||||
} 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.1.3")
|
||||
core = new Core(props, home, "0.1.10")
|
||||
} catch (Exception bad) {
|
||||
bad.printStackTrace(System.out)
|
||||
println "Failed to initialize core, exiting"
|
||||
|
@ -12,6 +12,7 @@ import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.connection.LeafConnectionManager
|
||||
import com.muwire.core.connection.UltrapeerConnectionManager
|
||||
import com.muwire.core.download.DownloadManager
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHashedEvent
|
||||
@ -90,9 +91,10 @@ public class Core {
|
||||
def i2pOptionsFile = new File(home,"i2p.properties")
|
||||
if (i2pOptionsFile.exists()) {
|
||||
i2pOptionsFile.withInputStream { i2pOptions.load(it) }
|
||||
if (!i2pOptions.hasProperty("inbound.nickname"))
|
||||
|
||||
if (!i2pOptions.containsKey("inbound.nickname"))
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
if (!i2pOptions.hasProperty("outbound.nickname"))
|
||||
if (!i2pOptions.containsKey("outbound.nickname"))
|
||||
i2pOptions["outbound.nickname"] = "MuWire"
|
||||
} else {
|
||||
i2pOptions["inbound.nickname"] = "MuWire"
|
||||
@ -101,13 +103,15 @@ public class Core {
|
||||
i2pOptions["inbound.quantity"] = "2"
|
||||
i2pOptions["outbound.length"] = "3"
|
||||
i2pOptions["outbound.quantity"] = "2"
|
||||
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
|
||||
i2pOptions["i2cp.tcp.port"] = "7654"
|
||||
}
|
||||
|
||||
// options like tunnel length and quantity
|
||||
I2PSession i2pSession
|
||||
I2PSocketManager socketManager
|
||||
keyDat.withInputStream {
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions)
|
||||
socketManager = new I2PSocketManagerFactory().createManager(it, i2pOptions["i2cp.tcp.host"], i2pOptions["i2cp.tcp.port"].toInteger(), i2pOptions)
|
||||
}
|
||||
socketManager.getDefaultOptions().setReadTimeout(60000)
|
||||
socketManager.getDefaultOptions().setConnectTimeout(30000)
|
||||
@ -156,7 +160,7 @@ public class Core {
|
||||
eventBus.register(SearchEvent.class, fileManager)
|
||||
|
||||
log.info "initializing persistence service"
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 5000, fileManager)
|
||||
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
|
||||
|
||||
log.info("initializing host cache")
|
||||
File hostStorage = new File(home, "hosts.json")
|
||||
@ -191,8 +195,11 @@ public class Core {
|
||||
eventBus.register(ResultsEvent.class, searchManager)
|
||||
|
||||
log.info("initializing download manager")
|
||||
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
|
||||
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, home, me)
|
||||
eventBus.register(UIDownloadEvent.class, downloadManager)
|
||||
eventBus.register(UILoadedEvent.class, downloadManager)
|
||||
eventBus.register(FileDownloadedEvent.class, downloadManager)
|
||||
eventBus.register(UIDownloadCancelledEvent.class, downloadManager)
|
||||
|
||||
log.info("initializing upload manager")
|
||||
UploadManager uploadManager = new UploadManager(eventBus, fileManager)
|
||||
@ -253,7 +260,7 @@ public class Core {
|
||||
}
|
||||
}
|
||||
|
||||
Core core = new Core(props, home, "0.1.3")
|
||||
Core core = new Core(props, home, "0.1.10")
|
||||
core.startServices()
|
||||
|
||||
// ... at the end, sleep or execute script
|
||||
|
@ -0,0 +1,4 @@
|
||||
package com.muwire.core
|
||||
|
||||
class UILoadedEvent extends Event {
|
||||
}
|
@ -108,6 +108,9 @@ class ConnectionAcceptor {
|
||||
case (byte)'G':
|
||||
processGET(e)
|
||||
break
|
||||
case (byte)'H':
|
||||
processHashList(e)
|
||||
break
|
||||
case (byte)'P':
|
||||
processPOST(e)
|
||||
break
|
||||
@ -178,9 +181,18 @@ class ConnectionAcceptor {
|
||||
dis.readFully(et)
|
||||
if (et != "ET ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid GET connection")
|
||||
uploadManager.processEndpoint(e)
|
||||
uploadManager.processGET(e)
|
||||
}
|
||||
|
||||
private void processHashList(Endpoint e) {
|
||||
byte[] ashList = new byte[8]
|
||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(ashList)
|
||||
if (ashList != "ASHLIST ".getBytes(StandardCharsets.US_ASCII))
|
||||
throw new IOException("Invalid HASHLIST connection")
|
||||
uploadManager.processHashList(e)
|
||||
}
|
||||
|
||||
private void processPOST(final Endpoint e) throws IOException {
|
||||
byte [] ost = new byte[4]
|
||||
final DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
|
@ -1,12 +1,21 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.connection.I2PConnector
|
||||
import com.muwire.core.files.FileDownloadedEvent
|
||||
import com.muwire.core.files.FileHasher
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import net.i2p.data.Base64
|
||||
import net.i2p.data.Destination
|
||||
import net.i2p.util.ConcurrentHashSet
|
||||
|
||||
import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.Persona
|
||||
import com.muwire.core.UILoadedEvent
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
@ -16,13 +25,16 @@ public class DownloadManager {
|
||||
private final EventBus eventBus
|
||||
private final I2PConnector connector
|
||||
private final Executor executor
|
||||
private final File incompletes
|
||||
private final File incompletes, home
|
||||
private final Persona me
|
||||
|
||||
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
|
||||
private final Set<Downloader> downloaders = new ConcurrentHashSet<>()
|
||||
|
||||
public DownloadManager(EventBus eventBus, I2PConnector connector, File home, Persona me) {
|
||||
this.eventBus = eventBus
|
||||
this.connector = connector
|
||||
this.incompletes = incompletes
|
||||
this.incompletes = new File(home,"incompletes")
|
||||
this.home = home
|
||||
this.me = me
|
||||
|
||||
incompletes.mkdir()
|
||||
@ -50,11 +62,77 @@ public class DownloadManager {
|
||||
def downloader = new Downloader(eventBus, this, me, e.target, size,
|
||||
infohash, pieceSize, connector, destinations,
|
||||
incompletes)
|
||||
downloaders.add(downloader)
|
||||
persistDownloaders()
|
||||
executor.execute({downloader.download()} as Runnable)
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
public void onUIDownloadCancelledEvent(UIDownloadCancelledEvent e) {
|
||||
downloaders.remove(e.downloader)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
void resume(Downloader downloader) {
|
||||
executor.execute({downloader.download() as Runnable})
|
||||
}
|
||||
|
||||
void onUILoadedEvent(UILoadedEvent e) {
|
||||
File downloadsFile = new File(home, "downloads.json")
|
||||
if (!downloadsFile.exists())
|
||||
return
|
||||
def slurper = new JsonSlurper()
|
||||
downloadsFile.eachLine {
|
||||
def json = slurper.parseText(it)
|
||||
File file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
|
||||
def destinations = new HashSet<>()
|
||||
json.destinations.each { destination ->
|
||||
destinations.add new Destination(destination)
|
||||
}
|
||||
InfoHash infoHash
|
||||
if (json.hashList != null) {
|
||||
byte[] hashList = Base64.decode(json.hashList)
|
||||
infoHash = InfoHash.fromHashList(hashList)
|
||||
} else {
|
||||
byte [] root = Base64.decode(json.hashRoot)
|
||||
infoHash = new InfoHash(root)
|
||||
}
|
||||
def downloader = new Downloader(eventBus, this, me, file, (long)json.length,
|
||||
infoHash, json.pieceSizePow2, connector, destinations, incompletes)
|
||||
downloaders.add(downloader)
|
||||
downloader.download()
|
||||
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
|
||||
}
|
||||
}
|
||||
|
||||
void onFileDownloadedEvent(FileDownloadedEvent e) {
|
||||
downloaders.remove(e.downloader)
|
||||
persistDownloaders()
|
||||
}
|
||||
|
||||
private void persistDownloaders() {
|
||||
File downloadsFile = new File(home,"downloads.json")
|
||||
downloadsFile.withPrintWriter { writer ->
|
||||
downloaders.each { downloader ->
|
||||
if (!downloader.cancelled) {
|
||||
def json = [:]
|
||||
json.file = Base64.encode(DataUtil.encodei18nString(downloader.file.getAbsolutePath()))
|
||||
json.length = downloader.length
|
||||
json.pieceSizePow2 = downloader.pieceSizePow2
|
||||
def destinations = []
|
||||
downloader.destinations.each {
|
||||
destinations << it.toBase64()
|
||||
}
|
||||
json.destinations = destinations
|
||||
|
||||
InfoHash infoHash = downloader.getInfoHash()
|
||||
if (infoHash.hashList != null)
|
||||
json.hashList = Base64.encode(infoHash.hashList)
|
||||
else
|
||||
json.hashRoot = Base64.encode(infoHash.getRoot())
|
||||
writer.println(JsonOutput.toJson(json))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ class DownloadSession {
|
||||
if (!code.startsWith("200 ")) {
|
||||
log.warning("unknown code $code")
|
||||
endpoint.close()
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// parse all headers
|
||||
@ -131,7 +131,7 @@ class DownloadSession {
|
||||
if (receivedStart != start || receivedEnd != end) {
|
||||
log.warning("We don't support mismatching ranges yet")
|
||||
endpoint.close()
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// start the download
|
||||
|
@ -20,8 +20,8 @@ import net.i2p.data.Destination
|
||||
|
||||
@Log
|
||||
public class Downloader {
|
||||
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||
private enum WorkerState { CONNECTING, DOWNLOADING, FINISHED}
|
||||
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, FINISHED }
|
||||
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
|
||||
|
||||
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
|
||||
Thread rv = new Thread(r)
|
||||
@ -36,12 +36,13 @@ public class Downloader {
|
||||
private final File file
|
||||
private final Pieces downloaded, claimed
|
||||
private final long length
|
||||
private final InfoHash infoHash
|
||||
private InfoHash infoHash
|
||||
private final int pieceSize
|
||||
private final I2PConnector connector
|
||||
private final Set<Destination> destinations
|
||||
private final int nPieces
|
||||
private final File piecesFile
|
||||
final int pieceSizePow2
|
||||
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
|
||||
|
||||
|
||||
@ -61,6 +62,7 @@ public class Downloader {
|
||||
this.connector = connector
|
||||
this.destinations = destinations
|
||||
this.piecesFile = new File(incompletes, file.getName()+".pieces")
|
||||
this.pieceSizePow2 = pieceSizePow2
|
||||
this.pieceSize = 1 << pieceSizePow2
|
||||
|
||||
int nPieces
|
||||
@ -74,6 +76,14 @@ public class Downloader {
|
||||
claimed = new Pieces(nPieces)
|
||||
}
|
||||
|
||||
public synchronized InfoHash getInfoHash() {
|
||||
infoHash
|
||||
}
|
||||
|
||||
private synchronized void setInfoHash(InfoHash infoHash) {
|
||||
this.infoHash = infoHash
|
||||
}
|
||||
|
||||
void download() {
|
||||
readPieces()
|
||||
destinations.each {
|
||||
@ -88,8 +98,8 @@ public class Downloader {
|
||||
void readPieces() {
|
||||
if (!piecesFile.exists())
|
||||
return
|
||||
piecesFile.withReader {
|
||||
int piece = Integer.parseInt(it.readLine())
|
||||
piecesFile.eachLine {
|
||||
int piece = Integer.parseInt(it)
|
||||
downloaded.markDownloaded(piece)
|
||||
}
|
||||
}
|
||||
@ -143,6 +153,17 @@ public class Downloader {
|
||||
if (oneDownloading)
|
||||
return DownloadState.DOWNLOADING
|
||||
|
||||
// at least one is requesting hashlist
|
||||
boolean oneHashlist = false
|
||||
activeWorkers.values().each {
|
||||
if (it.currentState == WorkerState.HASHLIST) {
|
||||
oneHashlist = true
|
||||
return
|
||||
}
|
||||
}
|
||||
if (oneHashlist)
|
||||
return DownloadState.HASHLIST
|
||||
|
||||
return DownloadState.CONNECTING
|
||||
}
|
||||
|
||||
@ -163,11 +184,18 @@ public class Downloader {
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
activeWorkers.each { destination, worker ->
|
||||
if (worker.currentState == WorkerState.FINISHED) {
|
||||
def newWorker = new DownloadWorker(destination)
|
||||
activeWorkers.put(destination, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
destinations.each { destination ->
|
||||
def worker = activeWorkers.get(destination)
|
||||
if (worker != null) {
|
||||
if (worker.currentState == WorkerState.FINISHED) {
|
||||
def newWorker = new DownloadWorker(destination)
|
||||
activeWorkers.put(destination, newWorker)
|
||||
executorService.submit(newWorker)
|
||||
}
|
||||
} else {
|
||||
worker = new DownloadWorker(destination)
|
||||
activeWorkers.put(destination, worker)
|
||||
executorService.submit(worker)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,10 +217,16 @@ public class Downloader {
|
||||
Endpoint endpoint = null
|
||||
try {
|
||||
endpoint = connector.connect(destination)
|
||||
while(getInfoHash().hashList == null) {
|
||||
currentState = WorkerState.HASHLIST
|
||||
HashListSession session = new HashListSession(me.toBase64(), infoHash, endpoint)
|
||||
InfoHash received = session.request()
|
||||
setInfoHash(received)
|
||||
}
|
||||
currentState = WorkerState.DOWNLOADING
|
||||
boolean requestPerformed
|
||||
while(!downloaded.isComplete()) {
|
||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
|
||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
|
||||
requestPerformed = currentSession.request()
|
||||
if (!requestPerformed)
|
||||
break
|
||||
@ -205,8 +239,12 @@ public class Downloader {
|
||||
if (downloaded.isComplete() && !eventFired) {
|
||||
piecesFile.delete()
|
||||
eventFired = true
|
||||
eventBus.publish(new FileDownloadedEvent(downloadedFile : new DownloadedFile(file, infoHash, pieceSize, Collections.emptySet())))
|
||||
}
|
||||
eventBus.publish(
|
||||
new FileDownloadedEvent(
|
||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
|
||||
downloader : Downloader.this))
|
||||
|
||||
}
|
||||
endpoint?.close()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
import groovy.util.logging.Log
|
||||
|
||||
import static com.muwire.core.util.DataUtil.readTillRN
|
||||
|
||||
import net.i2p.data.Base64
|
||||
|
||||
@Log
|
||||
class HashListSession {
|
||||
private final String meB64
|
||||
private final InfoHash infoHash
|
||||
private final Endpoint endpoint
|
||||
|
||||
HashListSession(String meB64, InfoHash infoHash, Endpoint endpoint) {
|
||||
this.meB64 = meB64
|
||||
this.infoHash = infoHash
|
||||
this.endpoint = endpoint
|
||||
}
|
||||
|
||||
InfoHash request() throws IOException {
|
||||
InputStream is = endpoint.getInputStream()
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
|
||||
String root = Base64.encode(infoHash.getRoot())
|
||||
os.write("HASHLIST $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
|
||||
String code = readTillRN(is)
|
||||
if (!code.startsWith("200"))
|
||||
throw new IOException("unknown code $code")
|
||||
|
||||
// parse all headers
|
||||
Set<String> headers = new HashSet<>()
|
||||
String header
|
||||
while((header = readTillRN(is)) != "" && headers.size() < Constants.MAX_HEADERS)
|
||||
headers.add(header)
|
||||
|
||||
long receivedStart = -1
|
||||
long receivedEnd = -1
|
||||
for (String receivedHeader : headers) {
|
||||
def group = (receivedHeader =~ /^Content-Range: (\d+)-(\d+)$/)
|
||||
if (group.size() != 1) {
|
||||
log.info("ignoring header $receivedHeader")
|
||||
continue
|
||||
}
|
||||
|
||||
receivedStart = Long.parseLong(group[0][1])
|
||||
receivedEnd = Long.parseLong(group[0][2])
|
||||
}
|
||||
|
||||
if (receivedStart != 0)
|
||||
throw new IOException("hashlist started at $receivedStart")
|
||||
|
||||
byte[] hashList = new byte[receivedEnd]
|
||||
ByteBuffer hashListBuf = ByteBuffer.wrap(hashList)
|
||||
byte[] tmp = new byte[0x1 << 13]
|
||||
while(hashListBuf.hasRemaining()) {
|
||||
if (hashListBuf.remaining() > tmp.length)
|
||||
tmp = new byte[hashListBuf.remaining()]
|
||||
int read = is.read(tmp)
|
||||
if (read == -1)
|
||||
throw new IOException()
|
||||
hashListBuf.put(tmp, 0, read)
|
||||
}
|
||||
|
||||
InfoHash received = InfoHash.fromHashList(hashList)
|
||||
if (received.getRoot() != infoHash.getRoot())
|
||||
throw new IOException("fetched list doesn't match root")
|
||||
received
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class UIDownloadCancelledEvent extends Event {
|
||||
Downloader downloader
|
||||
}
|
@ -2,10 +2,11 @@ package com.muwire.core.files
|
||||
|
||||
import com.muwire.core.DownloadedFile
|
||||
import com.muwire.core.Event
|
||||
import com.muwire.core.download.Downloader
|
||||
|
||||
import net.i2p.data.Destination
|
||||
|
||||
class FileDownloadedEvent extends Event {
|
||||
|
||||
Downloader downloader
|
||||
DownloadedFile downloadedFile
|
||||
}
|
||||
|
@ -20,12 +20,12 @@ class FileHasher {
|
||||
* @return the size of each piece in power of 2
|
||||
*/
|
||||
static int getPieceSize(long size) {
|
||||
if (size <= 0x1 << 27)
|
||||
if (size <= 0x1 << 30)
|
||||
return 17
|
||||
|
||||
for (int i = 28; i <= 37; i++) {
|
||||
for (int i = 31; i <= 37; i++) {
|
||||
if (size <= 0x1L << i) {
|
||||
return i-10
|
||||
return i-13
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import com.muwire.core.EventBus
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.SharedFile
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.search.ResultsEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
import com.muwire.core.search.SearchIndex
|
||||
@ -107,7 +108,7 @@ class FileManager {
|
||||
found = rootToFiles.get new InfoHash(e.searchHash)
|
||||
found = filter(found, e.oobInfohash)
|
||||
if (found != null && !found.isEmpty())
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid)
|
||||
re = new ResultsEvent(results: found.asList(), uuid: e.uuid, searchEvent: e)
|
||||
} else {
|
||||
def names = index.search e.searchTerms
|
||||
Set<File> files = new HashSet<>()
|
||||
@ -116,7 +117,7 @@ class FileManager {
|
||||
files.each { sharedFiles.add fileToSharedFile[it] }
|
||||
files = filter(sharedFiles, e.oobInfohash)
|
||||
if (!sharedFiles.isEmpty())
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid)
|
||||
re = new ResultsEvent(results: sharedFiles.asList(), uuid: e.uuid, searchEvent: e)
|
||||
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,9 @@ class PersisterService extends Service {
|
||||
} catch (IllegalArgumentException|NumberFormatException e) {
|
||||
log.log(Level.WARNING, "couldn't load files",e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listener.publish(new AllFilesLoadedEvent())
|
||||
}
|
||||
timer.schedule({persistFiles()} as TimerTask, 0, interval)
|
||||
loaded = true
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.muwire.core.SharedFile
|
||||
|
||||
class ResultsEvent extends Event {
|
||||
|
||||
SearchEvent searchEvent
|
||||
SharedFile[] results
|
||||
UUID uuid
|
||||
}
|
||||
|
@ -12,8 +12,19 @@ class ResultsParser {
|
||||
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
|
||||
if (json.type != "Result")
|
||||
throw new InvalidSearchResultException("not a result json")
|
||||
if (json.version != 1)
|
||||
throw new InvalidSearchResultException("unknown version $json.version")
|
||||
switch(json.version) {
|
||||
case 1:
|
||||
return parseV1(p, uuid, json)
|
||||
case 2:
|
||||
return parseV2(p, uuid, json)
|
||||
default:
|
||||
throw new InvalidSearchResultException("unknown version $json.version")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static parseV1(Persona p, UUID uuid, def json) {
|
||||
if (json.name == null)
|
||||
throw new InvalidSearchResultException("name missing")
|
||||
if (json.size == null)
|
||||
@ -52,4 +63,33 @@ class ResultsParser {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
}
|
||||
}
|
||||
|
||||
private static UIResultEvent parseV2(Persona p, UUID uuid, def json) {
|
||||
if (json.name == null)
|
||||
throw new InvalidSearchResultException("name missing")
|
||||
if (json.size == null)
|
||||
throw new InvalidSearchResultException("length missing")
|
||||
if (json.infohash == null)
|
||||
throw new InvalidSearchResultException("infohash missing")
|
||||
if (json.pieceSize == null)
|
||||
throw new InvalidSearchResultException("pieceSize missing")
|
||||
if (json.hashList != null)
|
||||
throw new InvalidSearchResultException("V2 result with hashlist")
|
||||
try {
|
||||
String name = DataUtil.readi18nString(Base64.decode(json.name))
|
||||
long size = json.size
|
||||
byte [] infoHash = Base64.decode(json.infohash)
|
||||
if (infoHash.length != InfoHash.SIZE)
|
||||
throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
|
||||
int pieceSize = json.pieceSize
|
||||
return new UIResultEvent( sender : p,
|
||||
name : name,
|
||||
size : size,
|
||||
infohash : new InfoHash(infoHash),
|
||||
pieceSize : pieceSize,
|
||||
uuid: uuid)
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSearchResultException("parsing search result failed",e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,8 +46,8 @@ class ResultsSender {
|
||||
this.me = me
|
||||
}
|
||||
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target) {
|
||||
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()}")
|
||||
void sendResults(UUID uuid, SharedFile[] results, Destination target, boolean oobInfohash) {
|
||||
log.info("Sending $results.length results for uuid $uuid to ${target.toBase32()} oobInfohash : $oobInfohash")
|
||||
if (target.equals(me.destination)) {
|
||||
results.each {
|
||||
long length = it.getFile().length()
|
||||
@ -64,7 +64,8 @@ class ResultsSender {
|
||||
eventBus.publish(uiResultEvent)
|
||||
}
|
||||
} else {
|
||||
executor.execute(new ResultSendJob(uuid : uuid, results : results, target: target))
|
||||
executor.execute(new ResultSendJob(uuid : uuid, results : results,
|
||||
target: target, oobInfohash : oobInfohash))
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,6 +73,7 @@ class ResultsSender {
|
||||
UUID uuid
|
||||
SharedFile [] results
|
||||
Destination target
|
||||
boolean oobInfohash
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
@ -94,19 +96,20 @@ class ResultsSender {
|
||||
String encodedName = Base64.encode(baos.toByteArray())
|
||||
def obj = [:]
|
||||
obj.type = "Result"
|
||||
obj.version = 1
|
||||
obj.version = oobInfohash ? 2 : 1
|
||||
obj.name = encodedName
|
||||
obj.infohash = Base64.encode(it.getInfoHash().getRoot())
|
||||
obj.size = it.getFile().length()
|
||||
obj.pieceSize = it.getPieceSize()
|
||||
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)
|
||||
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
|
||||
|
||||
def json = jsonOutput.toJson(obj)
|
||||
os.writeShort((short)json.length())
|
||||
os.write(json.getBytes(StandardCharsets.US_ASCII))
|
||||
|
@ -44,7 +44,7 @@ public class SearchManager {
|
||||
log.info("No results for search uuid $event.uuid")
|
||||
return
|
||||
}
|
||||
resultsSender.sendResults(event.uuid, event.results, target)
|
||||
resultsSender.sendResults(event.uuid, event.results, target, event.searchEvent.oobInfohash)
|
||||
}
|
||||
|
||||
boolean hasLocalSearch(UUID uuid) {
|
||||
|
@ -11,4 +11,9 @@ class UIResultEvent extends Event {
|
||||
long size
|
||||
InfoHash infohash
|
||||
int pieceSize
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
super.toString() + "name:$name size:$size sender:${sender.getHumanReadableName()} pieceSize $pieceSize"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package com.muwire.core.upload
|
||||
|
||||
class ContentRequest extends Request {
|
||||
Range range
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.muwire.core.upload
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
class ContentUploader extends Uploader {
|
||||
|
||||
private final File file
|
||||
private final ContentRequest request
|
||||
|
||||
ContentUploader(File file, ContentRequest request, Endpoint endpoint) {
|
||||
super(endpoint)
|
||||
this.file = file
|
||||
this.request = request
|
||||
}
|
||||
|
||||
@Override
|
||||
void respond() {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
Range range = request.getRange()
|
||||
if (range.start >= file.length() || range.end >= file.length()) {
|
||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
FileChannel channel
|
||||
try {
|
||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||
byte [] tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
int start = mapped.position()
|
||||
synchronized(this) {
|
||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||
}
|
||||
int read = mapped.position() - start
|
||||
endpoint.getOutputStream().write(tmp, 0, read)
|
||||
}
|
||||
} finally {
|
||||
try {channel?.close() } catch (IOException ignored) {}
|
||||
endpoint.getOutputStream().flush()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package com.muwire.core.upload
|
||||
|
||||
class HashListRequest extends Request {
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.muwire.core.upload
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import com.muwire.core.InfoHash
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
class HashListUploader extends Uploader {
|
||||
private final InfoHash infoHash
|
||||
private final HashListRequest request
|
||||
|
||||
HashListUploader(Endpoint endpoint, InfoHash infoHash, HashListRequest request) {
|
||||
super(endpoint)
|
||||
this.infoHash = infoHash
|
||||
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
||||
}
|
||||
|
||||
void respond() {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Content-Range: 0-${mapped.remaining()}\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
byte[]tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
int start = mapped.position()
|
||||
synchronized(this) {
|
||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||
}
|
||||
int read = mapped.position() - start
|
||||
endpoint.getOutputStream().write(tmp, 0, read)
|
||||
}
|
||||
endpoint.getOutputStream().flush()
|
||||
}
|
||||
}
|
@ -16,57 +16,12 @@ class Request {
|
||||
private static final byte N = "\n".getBytes(StandardCharsets.US_ASCII)[0]
|
||||
|
||||
InfoHash infoHash
|
||||
Range range
|
||||
Persona downloader
|
||||
Map<String, String> headers
|
||||
|
||||
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||
while(headers.size() < Constants.MAX_HEADERS) {
|
||||
boolean r = false
|
||||
boolean n = false
|
||||
int idx = 0
|
||||
while (true) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException("Stream closed")
|
||||
|
||||
if (!r && read == N)
|
||||
throw new IOException("Received N before R")
|
||||
if (read == R) {
|
||||
if (r)
|
||||
throw new IOException("double R")
|
||||
r = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (r && !n) {
|
||||
if (read != N)
|
||||
throw new IOException("R not followed by N")
|
||||
n = true
|
||||
break
|
||||
}
|
||||
if (idx == 0x1 << 14)
|
||||
throw new IOException("Header too long")
|
||||
tmp[idx++] = read
|
||||
}
|
||||
|
||||
if (idx == 0)
|
||||
break
|
||||
|
||||
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
||||
log.fine("Read header $header")
|
||||
|
||||
int keyIdx = header.indexOf(":")
|
||||
if (keyIdx < 1)
|
||||
throw new IOException("Header key not found")
|
||||
if (keyIdx == header.length())
|
||||
throw new IOException("Header value not found")
|
||||
String key = header.substring(0, keyIdx)
|
||||
String value = header.substring(keyIdx + 1)
|
||||
headers.put(key, value)
|
||||
}
|
||||
static Request parseContentRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
|
||||
Map<String, String> headers = parseHeaders(is)
|
||||
|
||||
if (!headers.containsKey("Range"))
|
||||
throw new IOException("Range header not found")
|
||||
@ -93,7 +48,69 @@ class Request {
|
||||
def decoded = Base64.decode(encoded)
|
||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||
}
|
||||
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
|
||||
new ContentRequest( infoHash : infoHash, range : new Range(start, end),
|
||||
headers : headers, downloader : downloader)
|
||||
}
|
||||
|
||||
static Request parseHashListRequest(InfoHash infoHash, InputStream is) throws IOException {
|
||||
Map<String,String> headers = parseHeaders(is)
|
||||
Persona downloader = null
|
||||
if (headers.containsKey("X-Persona")) {
|
||||
def encoded = headers["X-Persona"].trim()
|
||||
def decoded = Base64.decode(encoded)
|
||||
downloader = new Persona(new ByteArrayInputStream(decoded))
|
||||
}
|
||||
new HashListRequest(infoHash : infoHash, headers : headers, downloader : downloader)
|
||||
}
|
||||
|
||||
private static Map<String, String> parseHeaders(InputStream is) {
|
||||
Map<String,String> headers = new HashMap<>()
|
||||
byte [] tmp = new byte[Constants.MAX_HEADER_SIZE]
|
||||
while(headers.size() < Constants.MAX_HEADERS) {
|
||||
boolean r = false
|
||||
boolean n = false
|
||||
int idx = 0
|
||||
while (true) {
|
||||
byte read = is.read()
|
||||
if (read == -1)
|
||||
throw new IOException("Stream closed")
|
||||
|
||||
if (!r && read == N)
|
||||
throw new IOException("Received N before R")
|
||||
if (read == R) {
|
||||
if (r)
|
||||
throw new IOException("double R")
|
||||
r = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (r && !n) {
|
||||
if (read != N)
|
||||
throw new IOException("R not followed by N")
|
||||
n = true
|
||||
break
|
||||
}
|
||||
if (idx == 0x1 << 14)
|
||||
throw new IOException("Header too long")
|
||||
tmp[idx++] = read
|
||||
}
|
||||
|
||||
if (idx == 0)
|
||||
break
|
||||
|
||||
String header = new String(tmp, 0, idx, StandardCharsets.US_ASCII)
|
||||
log.fine("Read header $header")
|
||||
|
||||
int keyIdx = header.indexOf(":")
|
||||
if (keyIdx < 1)
|
||||
throw new IOException("Header key not found")
|
||||
if (keyIdx == header.length())
|
||||
throw new IOException("Header value not found")
|
||||
String key = header.substring(0, keyIdx)
|
||||
String value = header.substring(keyIdx + 1)
|
||||
headers.put(key, value)
|
||||
}
|
||||
headers
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ public class UploadManager {
|
||||
this.fileManager = fileManager
|
||||
}
|
||||
|
||||
public void processEndpoint(Endpoint e) throws IOException {
|
||||
public void processGET(Endpoint e) throws IOException {
|
||||
byte [] infoHashStringBytes = new byte[44]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
boolean first = true
|
||||
while(true) {
|
||||
if (first)
|
||||
@ -61,13 +61,13 @@ public class UploadManager {
|
||||
return
|
||||
}
|
||||
|
||||
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
Request request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||
log.info("Downloader persona doesn't match their destination")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
|
||||
Uploader uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
@ -75,7 +75,92 @@ public class UploadManager {
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void processHashList(Endpoint e) {
|
||||
byte [] infoHashStringBytes = new byte[44]
|
||||
DataInputStream dis = new DataInputStream(e.getInputStream())
|
||||
dis.readFully(infoHashStringBytes)
|
||||
String infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||
log.info("Responding to hashlist request for root $infoHashString")
|
||||
|
||||
byte [] infoHashRoot = Base64.decode(infoHashString)
|
||||
Set<SharedFile> sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
log.info "file not found"
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
byte [] rn = new byte[2]
|
||||
dis.readFully(rn)
|
||||
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||
log.warning("Malformed HASHLIST header")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
Request request = Request.parseHashListRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||
log.info("Downloader persona doesn't match their destination")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
Uploader uploader = new HashListUploader(e, sharedFiles.iterator().next().infoHash, request)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
} finally {
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
}
|
||||
|
||||
// proceed with content
|
||||
while(true) {
|
||||
byte[] get = new byte[4]
|
||||
dis.readFully(get)
|
||||
if (get != "GET ".getBytes(StandardCharsets.US_ASCII)) {
|
||||
log.warning("received a method other than GET on subsequent call")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
dis.readFully(infoHashStringBytes)
|
||||
infoHashString = new String(infoHashStringBytes, StandardCharsets.US_ASCII)
|
||||
log.info("Responding to upload request for root $infoHashString")
|
||||
|
||||
infoHashRoot = Base64.decode(infoHashString)
|
||||
sharedFiles = fileManager.getSharedFiles(infoHashRoot)
|
||||
if (sharedFiles == null || sharedFiles.isEmpty()) {
|
||||
log.info "file not found"
|
||||
e.getOutputStream().write("404 File Not Found\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
e.getOutputStream().flush()
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
rn = new byte[2]
|
||||
dis.readFully(rn)
|
||||
if (rn != "\r\n".getBytes(StandardCharsets.US_ASCII)) {
|
||||
log.warning("Malformed GET header")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
|
||||
request = Request.parseContentRequest(new InfoHash(infoHashRoot), e.getInputStream())
|
||||
if (request.downloader != null && request.downloader.destination != e.destination) {
|
||||
log.info("Downloader persona doesn't match their destination")
|
||||
e.close()
|
||||
return
|
||||
}
|
||||
uploader = new ContentUploader(sharedFiles.iterator().next().file, request, e)
|
||||
eventBus.publish(new UploadEvent(uploader : uploader))
|
||||
try {
|
||||
uploader.respond()
|
||||
} finally {
|
||||
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,49 +8,16 @@ import java.nio.file.StandardOpenOption
|
||||
|
||||
import com.muwire.core.connection.Endpoint
|
||||
|
||||
class Uploader {
|
||||
private final File file
|
||||
private final Request request
|
||||
private final Endpoint endpoint
|
||||
private ByteBuffer mapped
|
||||
abstract class Uploader {
|
||||
protected final Endpoint endpoint
|
||||
protected ByteBuffer mapped
|
||||
|
||||
Uploader(File file, Request request, Endpoint endpoint) {
|
||||
this.file = file
|
||||
this.request = request
|
||||
Uploader(Endpoint endpoint) {
|
||||
this.endpoint = endpoint
|
||||
}
|
||||
|
||||
void respond() {
|
||||
OutputStream os = endpoint.getOutputStream()
|
||||
Range range = request.getRange()
|
||||
if (range.start >= file.length() || range.end >= file.length()) {
|
||||
os.write("416 Range Not Satisfiable\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.flush()
|
||||
return
|
||||
}
|
||||
|
||||
os.write("200 OK\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
os.write("Content-Range: $range.start-$range.end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||
|
||||
FileChannel channel
|
||||
try {
|
||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ))
|
||||
mapped = channel.map(FileChannel.MapMode.READ_ONLY, range.start, range.end - range.start + 1)
|
||||
byte [] tmp = new byte[0x1 << 13]
|
||||
while(mapped.hasRemaining()) {
|
||||
int start = mapped.position()
|
||||
synchronized(this) {
|
||||
mapped.get(tmp, 0, Math.min(tmp.length, mapped.remaining()))
|
||||
}
|
||||
int read = mapped.position() - start
|
||||
endpoint.getOutputStream().write(tmp, 0, read)
|
||||
}
|
||||
} finally {
|
||||
try {channel?.close() } catch (IOException ignored) {}
|
||||
endpoint.getOutputStream().flush()
|
||||
}
|
||||
}
|
||||
|
||||
abstract void respond()
|
||||
|
||||
public synchronized int getPosition() {
|
||||
if (mapped == null)
|
||||
return -1
|
||||
|
@ -181,6 +181,8 @@ Search results are sent through and HTTP POST method from the responder to the o
|
||||
* The "altlocs" list contains list of alternate personas that the responder thinks may also have the file.
|
||||
* The "pieceSize" field is the size of the each individual file piece (except possibly the last) in powers of 2
|
||||
|
||||
Results version 1 contain the full hashlist, version 2 does not contain that list. See the "infohash-upgrade" document for more information.
|
||||
|
||||
### "Who do you trust" query - any node to any node
|
||||
(See the "web-of-trust" document for more info on this query)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
group = com.muwire
|
||||
version = 0.1.3
|
||||
version = 0.1.10
|
||||
groovyVersion = 2.4.15
|
||||
slf4jVersion = 1.7.25
|
||||
spockVersion = 1.1-groovy-2.4
|
||||
|
@ -15,6 +15,7 @@ import javax.inject.Inject
|
||||
import com.muwire.core.Constants
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.download.DownloadStartedEvent
|
||||
import com.muwire.core.download.UIDownloadCancelledEvent
|
||||
import com.muwire.core.download.UIDownloadEvent
|
||||
import com.muwire.core.search.QueryEvent
|
||||
import com.muwire.core.search.SearchEvent
|
||||
@ -52,7 +53,7 @@ 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: false)
|
||||
searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid, oobInfohash: true)
|
||||
}
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
@ -69,7 +70,8 @@ class MainFrameController {
|
||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||
model.results[uuid.toString()] = group
|
||||
|
||||
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid)
|
||||
def searchEvent = new SearchEvent(searchHash : Base64.decode(infoHash), uuid:uuid,
|
||||
oobInfohash: true)
|
||||
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
|
||||
replyTo: core.me.destination, receivedOn: core.me.destination,
|
||||
originator : core.me))
|
||||
@ -133,6 +135,7 @@ class MainFrameController {
|
||||
void cancel() {
|
||||
def downloader = model.downloads[selectedDownload()].downloader
|
||||
downloader.cancel()
|
||||
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
|
||||
}
|
||||
|
||||
@ControllerAction
|
||||
|
@ -66,6 +66,38 @@ class OptionsController {
|
||||
settings.write(it)
|
||||
}
|
||||
|
||||
// UI Setttings
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
text = view.lnfField.text
|
||||
model.lnf = text
|
||||
uiSettings.lnf = text
|
||||
|
||||
text = view.fontField.text
|
||||
model.font = text
|
||||
uiSettings.font = text
|
||||
|
||||
boolean showMonitor = view.monitorCheckbox.model.isSelected()
|
||||
model.showMonitor = showMonitor
|
||||
uiSettings.showMonitor = showMonitor
|
||||
|
||||
boolean clearCancelledDownloads = view.clearCancelledDownloadsCheckbox.model.isSelected()
|
||||
model.clearCancelledDownloads = clearCancelledDownloads
|
||||
uiSettings.clearCancelledDownloads = clearCancelledDownloads
|
||||
|
||||
boolean clearFinishedDownloads = view.clearFinishedDownloadsCheckbox.model.isSelected()
|
||||
model.clearFinishedDownloads = clearFinishedDownloads
|
||||
uiSettings.clearFinishedDownloads = clearFinishedDownloads
|
||||
|
||||
boolean excludeLocalResult = view.excludeLocalResultCheckbox.model.isSelected()
|
||||
model.excludeLocalResult = excludeLocalResult
|
||||
uiSettings.excludeLocalResult = excludeLocalResult
|
||||
|
||||
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||
uiSettingsFile.withOutputStream {
|
||||
uiSettings.write(it)
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,24 @@
|
||||
import griffon.core.GriffonApplication
|
||||
import groovy.util.logging.Log
|
||||
import net.i2p.util.SystemVersion
|
||||
|
||||
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.gui.UISettings
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
import javax.inject.Inject
|
||||
import javax.swing.JTable
|
||||
import javax.swing.LookAndFeel
|
||||
import javax.swing.UIManager
|
||||
|
||||
import static griffon.util.GriffonApplicationUtils.isMacOSX
|
||||
import static groovy.swing.SwingBuilder.lookAndFeel
|
||||
|
||||
import java.awt.Font
|
||||
|
||||
@Log
|
||||
class Initialize extends AbstractLifecycleHandler {
|
||||
@Inject
|
||||
@ -22,11 +28,82 @@ class Initialize extends AbstractLifecycleHandler {
|
||||
|
||||
@Override
|
||||
void execute() {
|
||||
if (isMacOSX()) {
|
||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||
} else {
|
||||
lookAndFeel('system', 'gtk')
|
||||
log.info "Loading home dir"
|
||||
def portableHome = System.getProperty("portable.home")
|
||||
def home = portableHome == null ?
|
||||
selectHome() :
|
||||
portableHome
|
||||
|
||||
home = new File(home)
|
||||
if (!home.exists()) {
|
||||
log.info("creating home dir $home")
|
||||
home.mkdirs()
|
||||
}
|
||||
|
||||
application.context.put("muwire-home", home.getAbsolutePath())
|
||||
|
||||
def guiPropsFile = new File(home, "gui.properties")
|
||||
UISettings uiSettings
|
||||
if (guiPropsFile.exists()) {
|
||||
Properties props = new Properties()
|
||||
guiPropsFile.withInputStream { props.load(it) }
|
||||
uiSettings = new UISettings(props)
|
||||
|
||||
log.info("settting user-specified lnf $uiSettings.lnf")
|
||||
lookAndFeel(uiSettings.lnf)
|
||||
|
||||
if (uiSettings.font != null) {
|
||||
log.info("setting user-specified font $uiSettings.font")
|
||||
Font font = new Font(uiSettings.font, Font.PLAIN, 12)
|
||||
def defaults = UIManager.getDefaults()
|
||||
defaults.put("Button.font", font)
|
||||
defaults.put("RadioButton.font", font)
|
||||
defaults.put("Label.font", font)
|
||||
defaults.put("CheckBox.font", font)
|
||||
defaults.put("Table.font", font)
|
||||
defaults.put("TableHeader.font", font)
|
||||
// TODO: add others
|
||||
}
|
||||
} else {
|
||||
Properties props = new Properties()
|
||||
uiSettings = new UISettings(props)
|
||||
log.info "will try default lnfs"
|
||||
if (isMacOSX()) {
|
||||
if (SystemVersion.isJava9()) {
|
||||
uiSettings.lnf = "metal"
|
||||
lookAndFeel("metal")
|
||||
} else {
|
||||
uiSettings.lnf = "nimbus"
|
||||
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
|
||||
}
|
||||
} else {
|
||||
LookAndFeel chosen = lookAndFeel('system', 'gtk')
|
||||
uiSettings.lnf = chosen.name
|
||||
log.info("ended up applying $chosen.name")
|
||||
}
|
||||
}
|
||||
|
||||
application.context.put("ui-settings", uiSettings)
|
||||
}
|
||||
|
||||
private static String selectHome() {
|
||||
def home = new File(System.properties["user.home"])
|
||||
def defaultHome = new File(home, ".MuWire")
|
||||
if (defaultHome.exists())
|
||||
return defaultHome.getAbsolutePath()
|
||||
if (SystemVersion.isMac()) {
|
||||
def library = new File(home, "Library")
|
||||
def appSupport = new File(library, "Application Support")
|
||||
def muwire = new File(appSupport,"MuWire")
|
||||
return muwire.getAbsolutePath()
|
||||
}
|
||||
if (SystemVersion.isWindows()) {
|
||||
def appData = new File(home,"AppData")
|
||||
def roaming = new File(appData, "Roaming")
|
||||
def muwire = new File(roaming, "MuWire")
|
||||
return muwire.getAbsolutePath()
|
||||
}
|
||||
defaultHome.getAbsolutePath()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
|
||||
|
||||
import com.muwire.core.Core
|
||||
import com.muwire.core.MuWireSettings
|
||||
import com.muwire.core.UILoadedEvent
|
||||
import com.muwire.core.files.FileSharedEvent
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
@ -33,17 +34,8 @@ class Ready extends AbstractLifecycleHandler {
|
||||
@Override
|
||||
void execute() {
|
||||
log.info "starting core services"
|
||||
def portableHome = System.getProperty("portable.home")
|
||||
def home = portableHome == null ?
|
||||
selectHome() :
|
||||
portableHome
|
||||
|
||||
home = new File(home)
|
||||
if (!home.exists()) {
|
||||
log.info("creating home dir $home")
|
||||
home.mkdirs()
|
||||
}
|
||||
|
||||
def home = new File(application.getContext().getAsString("muwire-home"))
|
||||
def props = new Properties()
|
||||
def propsFile = new File(home, "MuWire.properties")
|
||||
if (propsFile.exists()) {
|
||||
@ -117,26 +109,8 @@ class Ready extends AbstractLifecycleHandler {
|
||||
core.eventBus.publish(new FileSharedEvent(file : new File(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String selectHome() {
|
||||
def home = new File(System.properties["user.home"])
|
||||
def defaultHome = new File(home, ".MuWire")
|
||||
if (defaultHome.exists())
|
||||
return defaultHome.getAbsolutePath()
|
||||
if (SystemVersion.isMac()) {
|
||||
def library = new File(home, "Library")
|
||||
def appSupport = new File(library, "Application Support")
|
||||
def muwire = new File(appSupport,"MuWire")
|
||||
return muwire.getAbsolutePath()
|
||||
}
|
||||
if (SystemVersion.isWindows()) {
|
||||
def appData = new File(home,"AppData")
|
||||
def roaming = new File(appData, "Roaming")
|
||||
def muwire = new File(roaming, "MuWire")
|
||||
return muwire.getAbsolutePath()
|
||||
}
|
||||
defaultHome.getAbsolutePath()
|
||||
|
||||
core.eventBus.publish(new UILoadedEvent())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,11 +79,28 @@ class MainFrameModel {
|
||||
|
||||
void mvcGroupInit(Map<String, Object> args) {
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
|
||||
Timer timer = new Timer("download-pumper", true)
|
||||
timer.schedule({
|
||||
runInsideUIAsync {
|
||||
if (!mvcGroup.alive)
|
||||
return
|
||||
|
||||
// remove cancelled or finished downloads
|
||||
def toRemove = []
|
||||
downloads.each {
|
||||
if (uiSettings.clearCancelledDownloads &&
|
||||
it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED)
|
||||
toRemove << it
|
||||
if (uiSettings.clearFinishedDownloads &&
|
||||
it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED)
|
||||
toRemove << it
|
||||
}
|
||||
toRemove.each {
|
||||
downloads.remove(it)
|
||||
}
|
||||
|
||||
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
|
||||
|
||||
updateTablePreservingSelection("downloads-table")
|
||||
|
@ -20,6 +20,14 @@ class OptionsModel {
|
||||
@Observable String outboundLength
|
||||
@Observable String outboundQuantity
|
||||
|
||||
// gui options
|
||||
@Observable boolean showMonitor
|
||||
@Observable String lnf
|
||||
@Observable String font
|
||||
@Observable boolean clearCancelledDownloads
|
||||
@Observable boolean clearFinishedDownloads
|
||||
@Observable boolean excludeLocalResult
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
MuWireSettings settings = application.context.get("muwire-settings")
|
||||
downloadRetryInterval = settings.downloadRetryInterval
|
||||
@ -32,5 +40,13 @@ class OptionsModel {
|
||||
inboundQuantity = core.i2pOptions["inbound.quantity"]
|
||||
outboundLength = core.i2pOptions["outbound.length"]
|
||||
outboundQuantity = core.i2pOptions["outbound.quantity"]
|
||||
|
||||
UISettings uiSettings = application.context.get("ui-settings")
|
||||
showMonitor = uiSettings.showMonitor
|
||||
lnf = uiSettings.lnf
|
||||
font = uiSettings.font
|
||||
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||
excludeLocalResult = uiSettings.excludeLocalResult
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ class SearchTabModel {
|
||||
FactoryBuilderSupport builder
|
||||
|
||||
Core core
|
||||
UISettings uiSettings
|
||||
String uuid
|
||||
def results = []
|
||||
def hashBucket = [:]
|
||||
@ -26,6 +27,7 @@ class SearchTabModel {
|
||||
|
||||
void mvcGroupInit(Map<String, String> args) {
|
||||
core = mvcGroup.parentGroup.model.core
|
||||
uiSettings = application.context.get("ui-settings")
|
||||
mvcGroup.parentGroup.model.results[UUID.fromString(uuid)] = mvcGroup
|
||||
}
|
||||
|
||||
@ -34,6 +36,9 @@ class SearchTabModel {
|
||||
}
|
||||
|
||||
void handleResult(UIResultEvent e) {
|
||||
if (uiSettings.excludeLocalResult &&
|
||||
e.sender == core.me)
|
||||
return
|
||||
runInsideUIAsync {
|
||||
def bucket = hashBucket.get(e.infohash)
|
||||
if (bucket == null) {
|
||||
|
@ -41,6 +41,7 @@ class MainFrameView {
|
||||
def lastDownloadSortEvent
|
||||
|
||||
void initUI() {
|
||||
UISettings settings = application.context.get("ui-settings")
|
||||
builder.with {
|
||||
application(size : [1024,768], id: 'main-frame',
|
||||
locationRelativeTo : null,
|
||||
@ -63,7 +64,8 @@ class MainFrameView {
|
||||
gridLayout(rows:1, cols: 2)
|
||||
button(text: "Searches", actionPerformed : showSearchWindow)
|
||||
button(text: "Uploads", actionPerformed : showUploadsWindow)
|
||||
button(text: "Monitor", actionPerformed : showMonitorWindow)
|
||||
if (settings.showMonitor)
|
||||
button(text: "Monitor", actionPerformed : showMonitorWindow)
|
||||
button(text: "Trust", actionPerformed : showTrustWindow)
|
||||
}
|
||||
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
|
||||
@ -142,8 +144,7 @@ class MainFrameView {
|
||||
table(id : "shared-files-table", autoCreateRowSorter: true) {
|
||||
tableModel(list : model.shared) {
|
||||
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
|
||||
closureColumn(header : "Size", preferredWidth : 50, type : String,
|
||||
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
|
||||
closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -264,7 +265,9 @@ class MainFrameView {
|
||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
||||
selectionModel.addListSelectionListener({
|
||||
int selectedRow = selectedDownloaderRow()
|
||||
def downloader = model.downloads[selectedRow].downloader
|
||||
def downloader = model.downloads[selectedRow]?.downloader
|
||||
if (downloader == null)
|
||||
return
|
||||
switch(downloader.getCurrentState()) {
|
||||
case Downloader.DownloadState.CONNECTING :
|
||||
case Downloader.DownloadState.DOWNLOADING :
|
||||
@ -286,6 +289,10 @@ class MainFrameView {
|
||||
downloadsTable.setDefaultRenderer(Integer.class, centerRenderer)
|
||||
|
||||
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
||||
|
||||
// shared files table
|
||||
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
}
|
||||
|
||||
int selectedDownloaderRow() {
|
||||
|
@ -25,6 +25,8 @@ class OptionsView {
|
||||
def d
|
||||
def p
|
||||
def i
|
||||
def u
|
||||
|
||||
def retryField
|
||||
def updateField
|
||||
def allowUntrustedCheckbox
|
||||
@ -35,6 +37,13 @@ class OptionsView {
|
||||
def outboundLengthField
|
||||
def outboundQuantityField
|
||||
|
||||
def lnfField
|
||||
def monitorCheckbox
|
||||
def fontField
|
||||
def clearCancelledDownloadsCheckbox
|
||||
def clearFinishedDownloadsCheckbox
|
||||
def excludeLocalResultCheckbox
|
||||
|
||||
def buttonsPanel
|
||||
|
||||
def mainFrame
|
||||
@ -72,6 +81,22 @@ class OptionsView {
|
||||
label(text : "Outbound Quantity", constraints : gbc(gridx:0, gridy:4))
|
||||
outboundQuantityField = textField(text : bind {model.outboundQuantity}, columns : 2, constraints : gbc(gridx:1, gridy:4))
|
||||
}
|
||||
u = builder.panel {
|
||||
gridBagLayout()
|
||||
label(text : "Changing these settings requires a restart", constraints : gbc(gridx : 0, gridy : 0, gridwidth: 2))
|
||||
label(text : "Look And Feel", constraints : gbc(gridx: 0, gridy:1))
|
||||
lnfField = textField(text : bind {model.lnf}, columns : 4, constraints : gbc(gridx : 1, gridy : 1))
|
||||
label(text : "Font", constraints : gbc(gridx: 0, gridy : 2))
|
||||
fontField = textField(text : bind {model.font}, columns : 4, constraints : gbc(gridx : 1, gridy:2))
|
||||
label(text : "Show Monitor", constraints : gbc(gridx :0, gridy: 3))
|
||||
monitorCheckbox = checkBox(selected : bind {model.showMonitor}, constraints : gbc(gridx : 1, gridy: 3))
|
||||
label(text : "Clear Cancelled Downloads", constraints: gbc(gridx: 0, gridy:4))
|
||||
clearCancelledDownloadsCheckbox = checkBox(selected : bind {model.clearCancelledDownloads}, constraints : gbc(gridx : 1, gridy:4))
|
||||
label(text : "Clear Finished Downloads", constraints: gbc(gridx: 0, gridy:5))
|
||||
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, gridy:5))
|
||||
label(text : "Exclude Local Files From Results", constraints: gbc(gridx:0, gridy:6))
|
||||
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
|
||||
}
|
||||
buttonsPanel = builder.panel {
|
||||
gridBagLayout()
|
||||
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
|
||||
@ -81,8 +106,9 @@ class OptionsView {
|
||||
|
||||
void mvcGroupInit(Map<String,String> args) {
|
||||
def tabbedPane = new JTabbedPane()
|
||||
tabbedPane.addTab("MuWire Options", p)
|
||||
tabbedPane.addTab("I2P Options", i)
|
||||
tabbedPane.addTab("MuWire", p)
|
||||
tabbedPane.addTab("I2P", i)
|
||||
tabbedPane.addTab("GUI", u)
|
||||
|
||||
JPanel panel = new JPanel()
|
||||
panel.setLayout(new BorderLayout())
|
||||
|
@ -6,12 +6,17 @@ import griffon.inject.MVCMember
|
||||
import griffon.metadata.ArtifactProviderFor
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JTable
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import com.muwire.core.util.DataUtil
|
||||
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
|
||||
import javax.annotation.Nonnull
|
||||
|
||||
@ -35,7 +40,7 @@ class SearchTabView {
|
||||
resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
|
||||
tableModel(list: model.results) {
|
||||
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
|
||||
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
|
||||
closureColumn(header: "Size", preferredWidth: 50, type: Long, read : {row -> row.size})
|
||||
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
|
||||
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
|
||||
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
|
||||
@ -86,6 +91,9 @@ class SearchTabView {
|
||||
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
|
||||
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
|
||||
|
||||
resultsTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||
|
||||
|
||||
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
||||
}
|
||||
|
||||
|
29
gui/src/main/groovy/com/muwire/gui/SizeRenderer.groovy
Normal file
29
gui/src/main/groovy/com/muwire/gui/SizeRenderer.groovy
Normal file
@ -0,0 +1,29 @@
|
||||
package com.muwire.gui
|
||||
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JTable
|
||||
import javax.swing.table.DefaultTableCellRenderer
|
||||
|
||||
import net.i2p.data.DataHelper
|
||||
|
||||
class SizeRenderer extends DefaultTableCellRenderer {
|
||||
SizeRenderer() {
|
||||
setHorizontalAlignment(JLabel.CENTER)
|
||||
}
|
||||
@Override
|
||||
JComponent getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
Long l = (Long) value
|
||||
String formatted = DataHelper.formatSize2Decimal(l, false)+"B"
|
||||
setText(formatted)
|
||||
if (isSelected) {
|
||||
setForeground(table.getSelectionForeground())
|
||||
setBackground(table.getSelectionBackground())
|
||||
} else {
|
||||
setForeground(table.getForeground())
|
||||
setBackground(table.getBackground())
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
34
gui/src/main/groovy/com/muwire/gui/UISettings.groovy
Normal file
34
gui/src/main/groovy/com/muwire/gui/UISettings.groovy
Normal file
@ -0,0 +1,34 @@
|
||||
package com.muwire.gui
|
||||
|
||||
class UISettings {
|
||||
|
||||
String lnf
|
||||
boolean showMonitor
|
||||
String font
|
||||
boolean clearCancelledDownloads
|
||||
boolean clearFinishedDownloads
|
||||
boolean excludeLocalResult
|
||||
|
||||
UISettings(Properties props) {
|
||||
lnf = props.getProperty("lnf", "system")
|
||||
showMonitor = Boolean.parseBoolean(props.getProperty("showMonitor", "true"))
|
||||
font = props.getProperty("font",null)
|
||||
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
|
||||
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
|
||||
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
|
||||
}
|
||||
|
||||
void write(OutputStream out) throws IOException {
|
||||
Properties props = new Properties()
|
||||
props.setProperty("lnf", lnf)
|
||||
props.setProperty("showMonitor", String.valueOf(showMonitor))
|
||||
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
||||
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
||||
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
|
||||
if (font != null)
|
||||
props.setProperty("font", font)
|
||||
|
||||
|
||||
props.store(out, "UI Properties")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user