Compare commits
31 Commits
muwire-0.2
...
muwire-0.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0db7077a45 | ||
![]() |
614ecc85fe | ||
![]() |
af66a79376 | ||
![]() |
465171c81d | ||
![]() |
b507361c58 | ||
![]() |
4d001ae74b | ||
![]() |
36a6e2769f | ||
![]() |
69eeb7d77a | ||
![]() |
551982b72a | ||
![]() |
8d808f0b8f | ||
![]() |
7833a83c87 | ||
![]() |
3160c1a8f3 | ||
![]() |
e295aa67d5 | ||
![]() |
a9f5625dc3 | ||
![]() |
cc0af5b9ed | ||
![]() |
041fc3bef3 | ||
![]() |
03c3b1ebf1 | ||
![]() |
aece390daa | ||
![]() |
cf63be68e8 | ||
![]() |
88ece4dc23 | ||
![]() |
13767d58f2 | ||
![]() |
05a1ccd3d8 | ||
![]() |
6807c14a5f | ||
![]() |
684be0c50e | ||
![]() |
6655c262c6 | ||
![]() |
b1ccd55030 | ||
![]() |
a3becd0f7e | ||
![]() |
af2f3e0ebf | ||
![]() |
e2b7ffa1db | ||
![]() |
0e0176acfc | ||
![]() |
7f09bb079c |
42
TODO.md
Normal file
42
TODO.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# TODO List
|
||||||
|
|
||||||
|
Not in any particular order yet
|
||||||
|
|
||||||
|
### Big Items
|
||||||
|
|
||||||
|
##### Alternate Locations
|
||||||
|
|
||||||
|
This helps peers discover new sources for a file while the download is in progress. Also makes sharing of partial files possible.
|
||||||
|
|
||||||
|
##### Bloom Filters
|
||||||
|
|
||||||
|
This reduces query traffic by not sending last hop queries to peers that definitely do not have the file
|
||||||
|
|
||||||
|
##### Two-tier Topology
|
||||||
|
|
||||||
|
This helps with scalability
|
||||||
|
|
||||||
|
##### Trust List Sharing
|
||||||
|
|
||||||
|
For helping users make better decisions whom to trust
|
||||||
|
|
||||||
|
##### Content Control Panel
|
||||||
|
|
||||||
|
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple
|
||||||
|
|
||||||
|
##### Packaging With JRE, Embedded Router
|
||||||
|
|
||||||
|
For ease of deployment for new users, and so that users do not need to run a separate I2P router
|
||||||
|
|
||||||
|
##### Web UI, REST Interface, etc.
|
||||||
|
|
||||||
|
Basically any non-gui non-cli user interface
|
||||||
|
|
||||||
|
### Small Items
|
||||||
|
|
||||||
|
* Detect if router is dead and show warning or exit
|
||||||
|
* Wrapper of some kind for in-place upgrades
|
||||||
|
* Download file sequentially
|
||||||
|
* Unsharing of files
|
||||||
|
* Multiple-selection download, Ctrl-A
|
||||||
|
* Automatic sharing of new files in shared directories (more like medium item)
|
@@ -34,7 +34,7 @@ class Cli {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.2.0")
|
core = new Core(props, home, "0.2.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"
|
||||||
|
@@ -53,7 +53,7 @@ class CliDownloader {
|
|||||||
|
|
||||||
Core core
|
Core core
|
||||||
try {
|
try {
|
||||||
core = new Core(props, home, "0.2.0")
|
core = new Core(props, home, "0.2.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"
|
||||||
|
@@ -268,7 +268,7 @@ public class Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Core core = new Core(props, home, "0.2.0")
|
Core core = new Core(props, home, "0.2.4")
|
||||||
core.startServices()
|
core.startServices()
|
||||||
|
|
||||||
// ... at the end, sleep or execute script
|
// ... at the end, sleep or execute script
|
||||||
|
@@ -13,6 +13,7 @@ class MuWireSettings {
|
|||||||
String sharedFiles
|
String sharedFiles
|
||||||
CrawlerResponse crawlerResponse
|
CrawlerResponse crawlerResponse
|
||||||
boolean shareDownloadedFiles
|
boolean shareDownloadedFiles
|
||||||
|
boolean watchSharedDirectories
|
||||||
|
|
||||||
MuWireSettings() {
|
MuWireSettings() {
|
||||||
this(new Properties())
|
this(new Properties())
|
||||||
@@ -29,6 +30,7 @@ class MuWireSettings {
|
|||||||
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
|
||||||
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"))
|
||||||
|
watchSharedDirectories = Boolean.parseBoolean(props.getProperty("watchSharedDirectories","true"))
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
@@ -41,6 +43,7 @@ class MuWireSettings {
|
|||||||
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
|
||||||
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
|
||||||
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
|
||||||
|
props.setProperty("watchSharedDirectories", String.valueOf(watchSharedDirectories))
|
||||||
if (sharedFiles != null)
|
if (sharedFiles != null)
|
||||||
props.setProperty("sharedFiles", sharedFiles)
|
props.setProperty("sharedFiles", sharedFiles)
|
||||||
props.store(out, "")
|
props.store(out, "")
|
||||||
|
@@ -17,6 +17,8 @@ import com.muwire.core.upload.UploadManager
|
|||||||
import com.muwire.core.search.InvalidSearchResultException
|
import com.muwire.core.search.InvalidSearchResultException
|
||||||
import com.muwire.core.search.ResultsParser
|
import com.muwire.core.search.ResultsParser
|
||||||
import com.muwire.core.search.SearchManager
|
import com.muwire.core.search.SearchManager
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.search.UnexpectedResultsException
|
import com.muwire.core.search.UnexpectedResultsException
|
||||||
|
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
@@ -225,13 +227,15 @@ class ConnectionAcceptor {
|
|||||||
if (sender.destination != e.getDestination())
|
if (sender.destination != e.getDestination())
|
||||||
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
throw new IOException("Sender destination mismatch expected $e.getDestination(), got $sender.destination")
|
||||||
int nResults = dis.readUnsignedShort()
|
int nResults = dis.readUnsignedShort()
|
||||||
|
UIResultEvent[] results = new UIResultEvent[nResults]
|
||||||
for (int i = 0; i < nResults; i++) {
|
for (int i = 0; i < nResults; i++) {
|
||||||
int jsonSize = dis.readUnsignedShort()
|
int jsonSize = dis.readUnsignedShort()
|
||||||
byte [] payload = new byte[jsonSize]
|
byte [] payload = new byte[jsonSize]
|
||||||
dis.readFully(payload)
|
dis.readFully(payload)
|
||||||
def json = slurper.parse(payload)
|
def json = slurper.parse(payload)
|
||||||
eventBus.publish(ResultsParser.parse(sender, resultsUUID, json))
|
results[i] = ResultsParser.parse(sender, resultsUUID, json)
|
||||||
}
|
}
|
||||||
|
eventBus.publish(new UIResultBatchEvent(uuid: resultsUUID, results: results))
|
||||||
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
} catch (IOException | UnexpectedResultsException | InvalidSearchResultException bad) {
|
||||||
log.log(Level.WARNING, "failed to process POST", bad)
|
log.log(Level.WARNING, "failed to process POST", bad)
|
||||||
} finally {
|
} finally {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package com.muwire.core.connection
|
package com.muwire.core.connection
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
import groovy.util.logging.Log
|
import groovy.util.logging.Log
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
@@ -24,7 +25,7 @@ class Endpoint implements Closeable {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (!closed.compareAndSet(false, true)) {
|
if (!closed.compareAndSet(false, true)) {
|
||||||
log.warning("Close loop detected for ${destination.toBase32()}", new Exception())
|
log.log(Level.WARNING,"Close loop detected for ${destination.toBase32()}", new Exception())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (inputStream != null) {
|
if (inputStream != null) {
|
||||||
|
@@ -16,6 +16,7 @@ import java.nio.file.Files
|
|||||||
import java.nio.file.StandardOpenOption
|
import java.nio.file.StandardOpenOption
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.logging.Level
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
class DownloadSession {
|
class DownloadSession {
|
||||||
@@ -23,7 +24,7 @@ class DownloadSession {
|
|||||||
private static int SAMPLES = 10
|
private static int SAMPLES = 10
|
||||||
|
|
||||||
private final String meB64
|
private final String meB64
|
||||||
private final Pieces downloaded, claimed
|
private final Pieces pieces
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final Endpoint endpoint
|
private final Endpoint endpoint
|
||||||
private final File file
|
private final File file
|
||||||
@@ -36,11 +37,10 @@ class DownloadSession {
|
|||||||
|
|
||||||
private ByteBuffer mapped
|
private ByteBuffer mapped
|
||||||
|
|
||||||
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
|
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
|
||||||
int pieceSize, long fileLength) {
|
int pieceSize, long fileLength) {
|
||||||
this.meB64 = meB64
|
this.meB64 = meB64
|
||||||
this.downloaded = downloaded
|
this.pieces = pieces
|
||||||
this.claimed = claimed
|
|
||||||
this.endpoint = endpoint
|
this.endpoint = endpoint
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
this.file = file
|
this.file = file
|
||||||
@@ -63,20 +63,11 @@ class DownloadSession {
|
|||||||
OutputStream os = endpoint.getOutputStream()
|
OutputStream os = endpoint.getOutputStream()
|
||||||
InputStream is = endpoint.getInputStream()
|
InputStream is = endpoint.getInputStream()
|
||||||
|
|
||||||
int piece
|
int piece = pieces.claim()
|
||||||
while(true) {
|
if (piece == -1)
|
||||||
piece = downloaded.getRandomPiece()
|
return false
|
||||||
if (claimed.isMarked(piece)) {
|
boolean unclaim = true
|
||||||
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
|
|
||||||
log.info("all pieces claimed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
claimed.markDownloaded(piece)
|
|
||||||
|
|
||||||
log.info("will download piece $piece")
|
log.info("will download piece $piece")
|
||||||
|
|
||||||
long start = piece * pieceSize
|
long start = piece * pieceSize
|
||||||
@@ -85,7 +76,6 @@ class DownloadSession {
|
|||||||
|
|
||||||
String root = Base64.encode(infoHash.getRoot())
|
String root = Base64.encode(infoHash.getRoot())
|
||||||
|
|
||||||
FileChannel channel
|
|
||||||
try {
|
try {
|
||||||
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
|
||||||
@@ -135,41 +125,46 @@ class DownloadSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the download
|
// start the download
|
||||||
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
FileChannel channel
|
||||||
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
try {
|
||||||
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
|
||||||
byte[] tmp = new byte[0x1 << 13]
|
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
|
||||||
while(mapped.hasRemaining()) {
|
|
||||||
if (mapped.remaining() < tmp.length)
|
byte[] tmp = new byte[0x1 << 13]
|
||||||
tmp = new byte[mapped.remaining()]
|
while(mapped.hasRemaining()) {
|
||||||
int read = is.read(tmp)
|
if (mapped.remaining() < tmp.length)
|
||||||
if (read == -1)
|
tmp = new byte[mapped.remaining()]
|
||||||
throw new IOException()
|
int read = is.read(tmp)
|
||||||
synchronized(this) {
|
if (read == -1)
|
||||||
mapped.put(tmp, 0, read)
|
throw new IOException()
|
||||||
|
synchronized(this) {
|
||||||
if (timestamps.size() == SAMPLES) {
|
mapped.put(tmp, 0, read)
|
||||||
timestamps.removeFirst()
|
|
||||||
reads.removeFirst()
|
if (timestamps.size() == SAMPLES) {
|
||||||
|
timestamps.removeFirst()
|
||||||
|
reads.removeFirst()
|
||||||
|
}
|
||||||
|
timestamps.addLast(System.currentTimeMillis())
|
||||||
|
reads.addLast(read)
|
||||||
}
|
}
|
||||||
timestamps.addLast(System.currentTimeMillis())
|
|
||||||
reads.addLast(read)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapped.clear()
|
||||||
|
digest.update(mapped)
|
||||||
|
byte [] hash = digest.digest()
|
||||||
|
byte [] expected = new byte[32]
|
||||||
|
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
||||||
|
if (hash != expected)
|
||||||
|
throw new BadHashException()
|
||||||
|
} finally {
|
||||||
|
try { channel?.close() } catch (IOException ignore) {}
|
||||||
}
|
}
|
||||||
|
pieces.markDownloaded(piece)
|
||||||
mapped.clear()
|
unclaim = false
|
||||||
digest.update(mapped)
|
|
||||||
byte [] hash = digest.digest()
|
|
||||||
byte [] expected = new byte[32]
|
|
||||||
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
|
|
||||||
if (hash != expected)
|
|
||||||
throw new BadHashException()
|
|
||||||
|
|
||||||
downloaded.markDownloaded(piece)
|
|
||||||
} finally {
|
} finally {
|
||||||
claimed.clear(piece)
|
if (unclaim)
|
||||||
try { channel?.close() } catch (IOException ignore) {}
|
pieces.unclaim(piece)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import com.muwire.core.connection.Endpoint
|
|||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
import com.muwire.core.Constants
|
import com.muwire.core.Constants
|
||||||
@@ -34,7 +35,7 @@ public class Downloader {
|
|||||||
private final DownloadManager downloadManager
|
private final DownloadManager downloadManager
|
||||||
private final Persona me
|
private final Persona me
|
||||||
private final File file
|
private final File file
|
||||||
private final Pieces downloaded, claimed
|
private final Pieces pieces
|
||||||
private final long length
|
private final long length
|
||||||
private InfoHash infoHash
|
private InfoHash infoHash
|
||||||
private final int pieceSize
|
private final int pieceSize
|
||||||
@@ -47,7 +48,8 @@ public class Downloader {
|
|||||||
|
|
||||||
|
|
||||||
private volatile boolean cancelled
|
private volatile boolean cancelled
|
||||||
private volatile boolean eventFired
|
private final AtomicBoolean eventFired = new AtomicBoolean()
|
||||||
|
private boolean piecesFileClosed
|
||||||
|
|
||||||
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
public Downloader(EventBus eventBus, DownloadManager downloadManager,
|
||||||
Persona me, File file, long length, InfoHash infoHash,
|
Persona me, File file, long length, InfoHash infoHash,
|
||||||
@@ -72,8 +74,7 @@ public class Downloader {
|
|||||||
nPieces = length / pieceSize + 1
|
nPieces = length / pieceSize + 1
|
||||||
this.nPieces = nPieces
|
this.nPieces = nPieces
|
||||||
|
|
||||||
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
|
||||||
claimed = new Pieces(nPieces)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized InfoHash getInfoHash() {
|
public synchronized InfoHash getInfoHash() {
|
||||||
@@ -100,20 +101,24 @@ public class Downloader {
|
|||||||
return
|
return
|
||||||
piecesFile.eachLine {
|
piecesFile.eachLine {
|
||||||
int piece = Integer.parseInt(it)
|
int piece = Integer.parseInt(it)
|
||||||
downloaded.markDownloaded(piece)
|
pieces.markDownloaded(piece)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void writePieces() {
|
void writePieces() {
|
||||||
piecesFile.withPrintWriter { writer ->
|
synchronized(piecesFile) {
|
||||||
downloaded.getDownloaded().each { piece ->
|
if (piecesFileClosed)
|
||||||
writer.println(piece)
|
return
|
||||||
|
piecesFile.withPrintWriter { writer ->
|
||||||
|
pieces.getDownloaded().each { piece ->
|
||||||
|
writer.println(piece)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long donePieces() {
|
public long donePieces() {
|
||||||
downloaded.donePieces()
|
pieces.donePieces()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -136,7 +141,7 @@ public class Downloader {
|
|||||||
allFinished &= it.currentState == WorkerState.FINISHED
|
allFinished &= it.currentState == WorkerState.FINISHED
|
||||||
}
|
}
|
||||||
if (allFinished) {
|
if (allFinished) {
|
||||||
if (downloaded.isComplete())
|
if (pieces.isComplete())
|
||||||
return DownloadState.FINISHED
|
return DownloadState.FINISHED
|
||||||
return DownloadState.FAILED
|
return DownloadState.FAILED
|
||||||
}
|
}
|
||||||
@@ -171,7 +176,10 @@ public class Downloader {
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
stop()
|
stop()
|
||||||
file.delete()
|
file.delete()
|
||||||
piecesFile.delete()
|
synchronized(piecesFile) {
|
||||||
|
piecesFileClosed = true
|
||||||
|
piecesFile.delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
@@ -231,8 +239,8 @@ public class Downloader {
|
|||||||
}
|
}
|
||||||
currentState = WorkerState.DOWNLOADING
|
currentState = WorkerState.DOWNLOADING
|
||||||
boolean requestPerformed
|
boolean requestPerformed
|
||||||
while(!downloaded.isComplete()) {
|
while(!pieces.isComplete()) {
|
||||||
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
|
currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, file, pieceSize, length)
|
||||||
requestPerformed = currentSession.request()
|
requestPerformed = currentSession.request()
|
||||||
if (!requestPerformed)
|
if (!requestPerformed)
|
||||||
break
|
break
|
||||||
@@ -242,9 +250,11 @@ public class Downloader {
|
|||||||
log.log(Level.WARNING,"Exception while downloading",bad)
|
log.log(Level.WARNING,"Exception while downloading",bad)
|
||||||
} finally {
|
} finally {
|
||||||
currentState = WorkerState.FINISHED
|
currentState = WorkerState.FINISHED
|
||||||
if (downloaded.isComplete() && !eventFired) {
|
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
|
||||||
piecesFile.delete()
|
synchronized(piecesFile) {
|
||||||
eventFired = true
|
piecesFileClosed = true
|
||||||
|
piecesFile.delete()
|
||||||
|
}
|
||||||
eventBus.publish(
|
eventBus.publish(
|
||||||
new FileDownloadedEvent(
|
new FileDownloadedEvent(
|
||||||
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
|
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package com.muwire.core.download
|
package com.muwire.core.download
|
||||||
|
|
||||||
class Pieces {
|
class Pieces {
|
||||||
private final BitSet bitSet
|
private final BitSet done, claimed
|
||||||
private final int nPieces
|
private final int nPieces
|
||||||
private final float ratio
|
private final float ratio
|
||||||
private final Random random = new Random()
|
private final Random random = new Random()
|
||||||
@@ -13,52 +13,53 @@ class Pieces {
|
|||||||
Pieces(int nPieces, float ratio) {
|
Pieces(int nPieces, float ratio) {
|
||||||
this.nPieces = nPieces
|
this.nPieces = nPieces
|
||||||
this.ratio = ratio
|
this.ratio = ratio
|
||||||
bitSet = new BitSet(nPieces)
|
done = new BitSet(nPieces)
|
||||||
|
claimed = new BitSet(nPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int getRandomPiece() {
|
synchronized int claim() {
|
||||||
int cardinality = bitSet.cardinality()
|
int claimedCardinality = claimed.cardinality()
|
||||||
if (cardinality == nPieces)
|
if (claimedCardinality == nPieces)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
// if fuller than ratio just do sequential
|
// if fuller than ratio just do sequential
|
||||||
if ( (1.0f * cardinality) / nPieces > ratio) {
|
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
|
||||||
return bitSet.nextClearBit(0)
|
int rv = claimed.nextClearBit(0)
|
||||||
|
claimed.set(rv)
|
||||||
|
return rv
|
||||||
}
|
}
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
int start = random.nextInt(nPieces)
|
int start = random.nextInt(nPieces)
|
||||||
if (bitSet.get(start))
|
if (claimed.get(start))
|
||||||
continue
|
continue
|
||||||
|
claimed.set(start)
|
||||||
return start
|
return start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getDownloaded() {
|
synchronized def getDownloaded() {
|
||||||
def rv = []
|
def rv = []
|
||||||
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
|
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
|
||||||
rv << i
|
rv << i
|
||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void markDownloaded(int piece) {
|
synchronized void markDownloaded(int piece) {
|
||||||
bitSet.set(piece)
|
done.set(piece)
|
||||||
|
claimed.set(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void clear(int piece) {
|
synchronized void unclaim(int piece) {
|
||||||
bitSet.clear(piece)
|
claimed.clear(piece)
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized boolean isComplete() {
|
synchronized boolean isComplete() {
|
||||||
bitSet.cardinality() == nPieces
|
done.cardinality() == nPieces
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean isMarked(int piece) {
|
|
||||||
bitSet.get(piece)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized int donePieces() {
|
synchronized int donePieces() {
|
||||||
bitSet.cardinality()
|
done.cardinality()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
package com.muwire.core.files
|
||||||
|
|
||||||
|
class DirectoryWatcher {
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
package com.muwire.core.search
|
||||||
|
|
||||||
|
import com.muwire.core.Event
|
||||||
|
|
||||||
|
class UIResultBatchEvent extends Event {
|
||||||
|
UUID uuid
|
||||||
|
UIResultEvent[] results
|
||||||
|
}
|
@@ -51,4 +51,23 @@ class ContentUploader extends Uploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getProgress() {
|
||||||
|
if (mapped == null)
|
||||||
|
return 0
|
||||||
|
int position = mapped.position()
|
||||||
|
int total = request.getRange().end - request.getRange().start
|
||||||
|
(int)(position * 100.0 / total)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloader() {
|
||||||
|
request.downloader.getHumanReadableName()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,8 @@ import java.nio.charset.StandardCharsets
|
|||||||
import com.muwire.core.InfoHash
|
import com.muwire.core.InfoHash
|
||||||
import com.muwire.core.connection.Endpoint
|
import com.muwire.core.connection.Endpoint
|
||||||
|
|
||||||
|
import net.i2p.data.Base64
|
||||||
|
|
||||||
class HashListUploader extends Uploader {
|
class HashListUploader extends Uploader {
|
||||||
private final InfoHash infoHash
|
private final InfoHash infoHash
|
||||||
private final HashListRequest request
|
private final HashListRequest request
|
||||||
@@ -14,6 +16,7 @@ class HashListUploader extends Uploader {
|
|||||||
super(endpoint)
|
super(endpoint)
|
||||||
this.infoHash = infoHash
|
this.infoHash = infoHash
|
||||||
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
mapped = ByteBuffer.wrap(infoHash.getHashList())
|
||||||
|
this.request = request
|
||||||
}
|
}
|
||||||
|
|
||||||
void respond() {
|
void respond() {
|
||||||
@@ -32,4 +35,21 @@ class HashListUploader extends Uploader {
|
|||||||
}
|
}
|
||||||
endpoint.getOutputStream().flush()
|
endpoint.getOutputStream().flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Hash list for " + Base64.encode(infoHash.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getProgress() {
|
||||||
|
(int)(mapped.position() * 100.0 / mapped.capacity())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloader() {
|
||||||
|
request.downloader.getHumanReadableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -23,4 +23,13 @@ abstract class Uploader {
|
|||||||
return -1
|
return -1
|
||||||
mapped.position()
|
mapped.position()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an integer between 0 and 100
|
||||||
|
*/
|
||||||
|
abstract int getProgress();
|
||||||
|
|
||||||
|
abstract String getDownloader();
|
||||||
}
|
}
|
||||||
|
@@ -25,4 +25,17 @@ public class SharedFile {
|
|||||||
public int getPieceSize() {
|
public int getPieceSize() {
|
||||||
return pieceSize;
|
return pieceSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return file.hashCode() ^ infoHash.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof SharedFile))
|
||||||
|
return false;
|
||||||
|
SharedFile other = (SharedFile)o;
|
||||||
|
return file.equals(other.file) && infoHash.equals(other.infoHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
group = com.muwire
|
group = com.muwire
|
||||||
version = 0.2.0
|
version = 0.2.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
|
||||||
|
@@ -49,9 +49,20 @@ class MainFrameController {
|
|||||||
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
|
||||||
model.results[uuid.toString()] = group
|
model.results[uuid.toString()] = group
|
||||||
|
|
||||||
|
boolean hashSearch = false
|
||||||
|
byte [] root = null
|
||||||
|
if (search.length() == 44 && search.indexOf(" ") < 0) {
|
||||||
|
try {
|
||||||
|
root = Base64.decode(search)
|
||||||
|
hashSearch = true
|
||||||
|
} catch (Exception e) {
|
||||||
|
// not a hash search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def searchEvent
|
def searchEvent
|
||||||
if (model.hashSearch) {
|
if (hashSearch) {
|
||||||
searchEvent = new SearchEvent(searchHash : Base64.decode(search), uuid : uuid)
|
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true)
|
||||||
} else {
|
} else {
|
||||||
// this can be improved a lot
|
// this can be improved a lot
|
||||||
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
def replaced = search.toLowerCase().trim().replaceAll(Constants.SPLIT_PATTERN, " ")
|
||||||
@@ -175,16 +186,6 @@ class MainFrameController {
|
|||||||
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ControllerAction
|
|
||||||
void keywordSearch() {
|
|
||||||
model.hashSearch = false
|
|
||||||
}
|
|
||||||
|
|
||||||
@ControllerAction
|
|
||||||
void hashSearch() {
|
|
||||||
model.hashSearch = true
|
|
||||||
}
|
|
||||||
|
|
||||||
void unshareSelectedFiles() {
|
void unshareSelectedFiles() {
|
||||||
println "unsharing selected files"
|
println "unsharing selected files"
|
||||||
}
|
}
|
||||||
|
@@ -93,6 +93,10 @@ class OptionsController {
|
|||||||
model.excludeLocalResult = excludeLocalResult
|
model.excludeLocalResult = excludeLocalResult
|
||||||
uiSettings.excludeLocalResult = excludeLocalResult
|
uiSettings.excludeLocalResult = excludeLocalResult
|
||||||
|
|
||||||
|
boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected()
|
||||||
|
model.showSearchHashes = showSearchHashes
|
||||||
|
uiSettings.showSearchHashes = showSearchHashes
|
||||||
|
|
||||||
File uiSettingsFile = new File(core.home, "gui.properties")
|
File uiSettingsFile = new File(core.home, "gui.properties")
|
||||||
uiSettingsFile.withOutputStream {
|
uiSettingsFile.withOutputStream {
|
||||||
uiSettings.write(it)
|
uiSettings.write(it)
|
||||||
|
@@ -20,6 +20,7 @@ import com.muwire.core.files.FileHashedEvent
|
|||||||
import com.muwire.core.files.FileLoadedEvent
|
import com.muwire.core.files.FileLoadedEvent
|
||||||
import com.muwire.core.files.FileSharedEvent
|
import com.muwire.core.files.FileSharedEvent
|
||||||
import com.muwire.core.search.QueryEvent
|
import com.muwire.core.search.QueryEvent
|
||||||
|
import com.muwire.core.search.UIResultBatchEvent
|
||||||
import com.muwire.core.search.UIResultEvent
|
import com.muwire.core.search.UIResultEvent
|
||||||
import com.muwire.core.trust.TrustEvent
|
import com.muwire.core.trust.TrustEvent
|
||||||
import com.muwire.core.trust.TrustService
|
import com.muwire.core.trust.TrustService
|
||||||
@@ -34,6 +35,7 @@ import griffon.core.mvc.MVCGroup
|
|||||||
import griffon.inject.MVCMember
|
import griffon.inject.MVCMember
|
||||||
import griffon.transform.FXObservable
|
import griffon.transform.FXObservable
|
||||||
import griffon.transform.Observable
|
import griffon.transform.Observable
|
||||||
|
import net.i2p.data.Base64
|
||||||
import net.i2p.data.Destination
|
import net.i2p.data.Destination
|
||||||
import griffon.metadata.ArtifactProviderFor
|
import griffon.metadata.ArtifactProviderFor
|
||||||
|
|
||||||
@@ -56,8 +58,6 @@ class MainFrameModel {
|
|||||||
def trusted = []
|
def trusted = []
|
||||||
def distrusted = []
|
def distrusted = []
|
||||||
|
|
||||||
boolean hashSearch
|
|
||||||
|
|
||||||
@Observable int connections
|
@Observable int connections
|
||||||
@Observable String me
|
@Observable String me
|
||||||
@Observable boolean searchButtonsEnabled
|
@Observable boolean searchButtonsEnabled
|
||||||
@@ -69,6 +69,8 @@ class MainFrameModel {
|
|||||||
volatile Core core
|
volatile Core core
|
||||||
|
|
||||||
private long lastRetryTime = System.currentTimeMillis()
|
private long lastRetryTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
UISettings uiSettings
|
||||||
|
|
||||||
void updateTablePreservingSelection(String tableName) {
|
void updateTablePreservingSelection(String tableName) {
|
||||||
def downloadTable = builder.getVariable(tableName)
|
def downloadTable = builder.getVariable(tableName)
|
||||||
@@ -79,7 +81,7 @@ class MainFrameModel {
|
|||||||
|
|
||||||
void mvcGroupInit(Map<String, Object> args) {
|
void mvcGroupInit(Map<String, Object> args) {
|
||||||
|
|
||||||
UISettings uiSettings = application.context.get("ui-settings")
|
uiSettings = application.context.get("ui-settings")
|
||||||
|
|
||||||
Timer timer = new Timer("download-pumper", true)
|
Timer timer = new Timer("download-pumper", true)
|
||||||
timer.schedule({
|
timer.schedule({
|
||||||
@@ -114,6 +116,7 @@ class MainFrameModel {
|
|||||||
core = e.getNewValue()
|
core = e.getNewValue()
|
||||||
me = core.me.getHumanReadableName()
|
me = core.me.getHumanReadableName()
|
||||||
core.eventBus.register(UIResultEvent.class, this)
|
core.eventBus.register(UIResultEvent.class, this)
|
||||||
|
core.eventBus.register(UIResultBatchEvent.class, this)
|
||||||
core.eventBus.register(DownloadStartedEvent.class, this)
|
core.eventBus.register(DownloadStartedEvent.class, this)
|
||||||
core.eventBus.register(ConnectionEvent.class, this)
|
core.eventBus.register(ConnectionEvent.class, this)
|
||||||
core.eventBus.register(DisconnectionEvent.class, this)
|
core.eventBus.register(DisconnectionEvent.class, this)
|
||||||
@@ -160,6 +163,11 @@ class MainFrameModel {
|
|||||||
resultsGroup?.model.handleResult(e)
|
resultsGroup?.model.handleResult(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onUIResultBatchEvent(UIResultBatchEvent e) {
|
||||||
|
MVCGroup resultsGroup = results.get(e.uuid)
|
||||||
|
resultsGroup?.model.handleResultBatch(e.results)
|
||||||
|
}
|
||||||
|
|
||||||
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
void onDownloadStartedEvent(DownloadStartedEvent e) {
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
downloads << e
|
downloads << e
|
||||||
@@ -177,7 +185,8 @@ class MainFrameModel {
|
|||||||
topPanel.getLayout().show(topPanel, "top-search-panel")
|
topPanel.getLayout().show(topPanel, "top-search-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionList.add(e.endpoint.destination)
|
UIConnection con = new UIConnection(destination : e.endpoint.destination, incoming : e.incoming)
|
||||||
|
connectionList.add(con)
|
||||||
JTable table = builder.getVariable("connections-table")
|
JTable table = builder.getVariable("connections-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
@@ -192,7 +201,8 @@ class MainFrameModel {
|
|||||||
topPanel.getLayout().show(topPanel, "top-connect-panel")
|
topPanel.getLayout().show(topPanel, "top-connect-panel")
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionList.remove(e.destination)
|
UIConnection con = new UIConnection(destination : e.destination)
|
||||||
|
connectionList.remove(con)
|
||||||
JTable table = builder.getVariable("connections-table")
|
JTable table = builder.getVariable("connections-table")
|
||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
@@ -258,14 +268,23 @@ class MainFrameModel {
|
|||||||
void onQueryEvent(QueryEvent e) {
|
void onQueryEvent(QueryEvent e) {
|
||||||
if (e.replyTo == core.me.destination)
|
if (e.replyTo == core.me.destination)
|
||||||
return
|
return
|
||||||
StringBuilder sb = new StringBuilder()
|
|
||||||
e.searchEvent.searchTerms?.each {
|
def search
|
||||||
sb.append(it)
|
if (e.searchEvent.searchHash != null) {
|
||||||
sb.append(" ")
|
if (!uiSettings.showSearchHashes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
search = Base64.encode(e.searchEvent.searchHash)
|
||||||
|
} else {
|
||||||
|
StringBuilder sb = new StringBuilder()
|
||||||
|
e.searchEvent.searchTerms?.each {
|
||||||
|
sb.append(it)
|
||||||
|
sb.append(" ")
|
||||||
|
}
|
||||||
|
search = sb.toString()
|
||||||
|
if (search.trim().size() == 0)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
def search = sb.toString()
|
|
||||||
if (search.trim().size() == 0)
|
|
||||||
return
|
|
||||||
runInsideUIAsync {
|
runInsideUIAsync {
|
||||||
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
|
||||||
while(searches.size() > 200)
|
while(searches.size() > 200)
|
||||||
@@ -303,4 +322,22 @@ class MainFrameModel {
|
|||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class UIConnection {
|
||||||
|
Destination destination
|
||||||
|
boolean incoming
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
destination.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof UIConnection))
|
||||||
|
return false
|
||||||
|
UIConnection other = (UIConnection) o
|
||||||
|
return destination == other.destination
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -27,6 +27,7 @@ class OptionsModel {
|
|||||||
@Observable boolean clearCancelledDownloads
|
@Observable boolean clearCancelledDownloads
|
||||||
@Observable boolean clearFinishedDownloads
|
@Observable boolean clearFinishedDownloads
|
||||||
@Observable boolean excludeLocalResult
|
@Observable boolean excludeLocalResult
|
||||||
|
@Observable boolean showSearchHashes
|
||||||
|
|
||||||
void mvcGroupInit(Map<String, String> args) {
|
void mvcGroupInit(Map<String, String> args) {
|
||||||
MuWireSettings settings = application.context.get("muwire-settings")
|
MuWireSettings settings = application.context.get("muwire-settings")
|
||||||
@@ -48,5 +49,6 @@ class OptionsModel {
|
|||||||
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
clearCancelledDownloads = uiSettings.clearCancelledDownloads
|
||||||
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
clearFinishedDownloads = uiSettings.clearFinishedDownloads
|
||||||
excludeLocalResult = uiSettings.excludeLocalResult
|
excludeLocalResult = uiSettings.excludeLocalResult
|
||||||
|
showSearchHashes = uiSettings.showSearchHashes
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -52,4 +52,22 @@ class SearchTabModel {
|
|||||||
table.model.fireTableDataChanged()
|
table.model.fireTableDataChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleResultBatch(UIResultEvent[] batch) {
|
||||||
|
runInsideUIAsync {
|
||||||
|
batch.each {
|
||||||
|
if (uiSettings.excludeLocalResult && it.sender == core.me)
|
||||||
|
return
|
||||||
|
def bucket = hashBucket.get(it.infohash)
|
||||||
|
if (bucket == null) {
|
||||||
|
bucket = []
|
||||||
|
hashBucket[it.infohash] = bucket
|
||||||
|
}
|
||||||
|
bucket << it
|
||||||
|
results << it
|
||||||
|
}
|
||||||
|
JTable table = builder.getVariable("results-table")
|
||||||
|
table.model.fireTableDataChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -3,6 +3,7 @@ package com.muwire.gui
|
|||||||
import griffon.core.artifact.GriffonView
|
import griffon.core.artifact.GriffonView
|
||||||
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.DataHelper
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
import javax.swing.BorderFactory
|
import javax.swing.BorderFactory
|
||||||
@@ -13,6 +14,7 @@ import javax.swing.JLabel
|
|||||||
import javax.swing.JMenuItem
|
import javax.swing.JMenuItem
|
||||||
import javax.swing.JPopupMenu
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.JSplitPane
|
import javax.swing.JSplitPane
|
||||||
|
import javax.swing.JTable
|
||||||
import javax.swing.ListSelectionModel
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
import javax.swing.border.Border
|
import javax.swing.border.Border
|
||||||
@@ -28,6 +30,8 @@ import java.awt.FlowLayout
|
|||||||
import java.awt.GridBagConstraints
|
import java.awt.GridBagConstraints
|
||||||
import java.awt.GridBagLayout
|
import java.awt.GridBagLayout
|
||||||
import java.awt.Insets
|
import java.awt.Insets
|
||||||
|
import java.awt.Toolkit
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
@@ -43,6 +47,7 @@ class MainFrameView {
|
|||||||
|
|
||||||
def downloadsTable
|
def downloadsTable
|
||||||
def lastDownloadSortEvent
|
def lastDownloadSortEvent
|
||||||
|
def lastSharedSortEvent
|
||||||
|
|
||||||
void initUI() {
|
void initUI() {
|
||||||
UISettings settings = application.context.get("ui-settings")
|
UISettings settings = application.context.get("ui-settings")
|
||||||
@@ -85,12 +90,6 @@ class MainFrameView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
panel( constraints: BorderLayout.EAST) {
|
panel( constraints: BorderLayout.EAST) {
|
||||||
panel {
|
|
||||||
buttonGroup(id : "searchButtonGroup")
|
|
||||||
radioButton(text : "Keywords", selected : true, buttonGroup : searchButtonGroup, keywordSearchAction)
|
|
||||||
radioButton(text : "Hash", selected : false, buttonGroup : searchButtonGroup, hashSearchAction)
|
|
||||||
|
|
||||||
}
|
|
||||||
button(text: "Search", searchAction)
|
button(text: "Search", searchAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +120,7 @@ class MainFrameView {
|
|||||||
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
|
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
|
||||||
int pieces = row.downloader.nPieces
|
int pieces = row.downloader.nPieces
|
||||||
int done = row.downloader.donePieces()
|
int done = row.downloader.donePieces()
|
||||||
"$done/$pieces pieces"
|
"$done/$pieces pieces".toString()
|
||||||
})
|
})
|
||||||
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
|
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
|
||||||
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
|
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
|
||||||
@@ -161,16 +160,13 @@ class MainFrameView {
|
|||||||
scrollPane (constraints : BorderLayout.CENTER) {
|
scrollPane (constraints : BorderLayout.CENTER) {
|
||||||
table(id : "uploads-table") {
|
table(id : "uploads-table") {
|
||||||
tableModel(list : model.uploads) {
|
tableModel(list : model.uploads) {
|
||||||
closureColumn(header : "Name", type : String, read : {row -> row.file.getName() })
|
closureColumn(header : "Name", type : String, read : {row -> row.getName() })
|
||||||
closureColumn(header : "Progress", type : String, read : { row ->
|
closureColumn(header : "Progress", type : String, read : { row ->
|
||||||
int position = row.getPosition()
|
int percent = row.getProgress()
|
||||||
def range = row.request.getRange()
|
|
||||||
int total = range.end - range.start
|
|
||||||
int percent = (int)((position * 100.0) / total)
|
|
||||||
"$percent%"
|
"$percent%"
|
||||||
})
|
})
|
||||||
closureColumn(header : "Downloader", type : String, read : { row ->
|
closureColumn(header : "Downloader", type : String, read : { row ->
|
||||||
row.request.downloader?.getHumanReadableName()
|
row.getDownloader()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +183,13 @@ class MainFrameView {
|
|||||||
scrollPane(constraints : BorderLayout.CENTER) {
|
scrollPane(constraints : BorderLayout.CENTER) {
|
||||||
table(id : "connections-table") {
|
table(id : "connections-table") {
|
||||||
tableModel(list : model.connectionList) {
|
tableModel(list : model.connectionList) {
|
||||||
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
|
closureColumn(header : "Destination", preferredWidth: 250, type: String, read : { row -> row.destination.toBase32() })
|
||||||
|
closureColumn(header : "Direction", preferredWidth: 20, type: String, read : { row ->
|
||||||
|
if (row.incoming)
|
||||||
|
return "In"
|
||||||
|
else
|
||||||
|
return "Out"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,14 +303,32 @@ class MainFrameView {
|
|||||||
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
downloadsTable.rowSorter.addRowSorterListener({evt -> lastDownloadSortEvent = evt})
|
||||||
downloadsTable.rowSorter.setSortsOnUpdates(true)
|
downloadsTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
|
||||||
|
downloadsTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showDownloadsMenu(e)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showDownloadsMenu(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// shared files table
|
// shared files table
|
||||||
def sharedFilesTable = builder.getVariable("shared-files-table")
|
def sharedFilesTable = builder.getVariable("shared-files-table")
|
||||||
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer())
|
||||||
|
|
||||||
|
sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt})
|
||||||
|
sharedFilesTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
|
||||||
JPopupMenu sharedFilesMenu = new JPopupMenu()
|
JPopupMenu sharedFilesMenu = new JPopupMenu()
|
||||||
JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
|
// JMenuItem unshareSelectedFiles = new JMenuItem("Unshare selected files")
|
||||||
unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
|
// unshareSelectedFiles.addActionListener({mvcGroup.controller.unshareSelectedFiles()})
|
||||||
sharedFilesMenu.add(unshareSelectedFiles)
|
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||||
|
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard(sharedFilesTable)})
|
||||||
|
sharedFilesMenu.add(copyHashToClipboard)
|
||||||
sharedFilesTable.addMouseListener(new MouseAdapter() {
|
sharedFilesTable.addMouseListener(new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void mouseReleased(MouseEvent e) {
|
public void mouseReleased(MouseEvent e) {
|
||||||
@@ -321,10 +341,52 @@ class MainFrameView {
|
|||||||
showPopupMenu(sharedFilesMenu, e)
|
showPopupMenu(sharedFilesMenu, e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// searches table
|
||||||
|
def searchesTable = builder.getVariable("searches-table")
|
||||||
|
JPopupMenu searchTableMenu = new JPopupMenu()
|
||||||
|
JMenuItem copySearchToClipboard = new JMenuItem("Copy search to clipboard")
|
||||||
|
copySearchToClipboard.addActionListener({mvcGroup.view.copySearchToClipboard(searchesTable)})
|
||||||
|
searchTableMenu.add(copySearchToClipboard)
|
||||||
|
searchesTable.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(searchTableMenu, e)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void mousePressed(MouseEvent e) {
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
showPopupMenu(searchTableMenu, e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def showPopupMenu(JPopupMenu menu, MouseEvent event) {
|
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {
|
||||||
// menu.show(event.getComponent(), event.getX(), event.getY())
|
menu.show(event.getComponent(), event.getX(), event.getY())
|
||||||
|
}
|
||||||
|
|
||||||
|
def copyHashToClipboard(JTable sharedFilesTable) {
|
||||||
|
int selected = sharedFilesTable.getSelectedRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
if (lastSharedSortEvent != null)
|
||||||
|
selected = sharedFilesTable.rowSorter.convertRowIndexToModel(selected)
|
||||||
|
String root = Base64.encode(model.shared[selected].infoHash.getRoot())
|
||||||
|
StringSelection selection = new StringSelection(root)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
def copySearchToClipboard(JTable searchesTable) {
|
||||||
|
int selected = searchesTable.getSelectedRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
String search = model.searches[selected].search
|
||||||
|
StringSelection selection = new StringSelection(search)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
int selectedDownloaderRow() {
|
int selectedDownloaderRow() {
|
||||||
@@ -334,6 +396,54 @@ class MainFrameView {
|
|||||||
selected
|
selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def showDownloadsMenu(MouseEvent e) {
|
||||||
|
int selected = selectedDownloaderRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
boolean cancelEnabled = false
|
||||||
|
boolean retryEnabled = false
|
||||||
|
Downloader downloader = model.downloads[selected].downloader
|
||||||
|
switch(downloader.currentState) {
|
||||||
|
case Downloader.DownloadState.DOWNLOADING:
|
||||||
|
case Downloader.DownloadState.HASHLIST:
|
||||||
|
case Downloader.DownloadState.CONNECTING:
|
||||||
|
cancelEnabled = true
|
||||||
|
retryEnabled = false
|
||||||
|
break
|
||||||
|
case Downloader.DownloadState.FAILED:
|
||||||
|
cancelEnabled = true
|
||||||
|
retryEnabled = true
|
||||||
|
break
|
||||||
|
default :
|
||||||
|
cancelEnabled = false
|
||||||
|
retryEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
JPopupMenu menu = new JPopupMenu()
|
||||||
|
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||||
|
copyHashToClipboard.addActionListener({
|
||||||
|
String hash = Base64.encode(downloader.infoHash.getRoot())
|
||||||
|
StringSelection selection = new StringSelection(hash)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
|
})
|
||||||
|
menu.add(copyHashToClipboard)
|
||||||
|
|
||||||
|
if (cancelEnabled) {
|
||||||
|
JMenuItem cancel = new JMenuItem("Cancel")
|
||||||
|
cancel.addActionListener({mvcGroup.controller.cancel()})
|
||||||
|
menu.add(cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryEnabled) {
|
||||||
|
JMenuItem retry = new JMenuItem("Retry")
|
||||||
|
retry.addActionListener({mvcGroup.controller.resume()})
|
||||||
|
menu.add(retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopupMenu(menu, e)
|
||||||
|
}
|
||||||
|
|
||||||
def showSearchWindow = {
|
def showSearchWindow = {
|
||||||
def cardsPanel = builder.getVariable("cards-panel")
|
def cardsPanel = builder.getVariable("cards-panel")
|
||||||
cardsPanel.getLayout().show(cardsPanel, "search window")
|
cardsPanel.getLayout().show(cardsPanel, "search window")
|
||||||
|
@@ -43,6 +43,7 @@ class OptionsView {
|
|||||||
def clearCancelledDownloadsCheckbox
|
def clearCancelledDownloadsCheckbox
|
||||||
def clearFinishedDownloadsCheckbox
|
def clearFinishedDownloadsCheckbox
|
||||||
def excludeLocalResultCheckbox
|
def excludeLocalResultCheckbox
|
||||||
|
def showSearchHashesCheckbox
|
||||||
|
|
||||||
def buttonsPanel
|
def buttonsPanel
|
||||||
|
|
||||||
@@ -96,6 +97,8 @@ class OptionsView {
|
|||||||
clearFinishedDownloadsCheckbox = checkBox(selected : bind {model.clearFinishedDownloads}, constraints : gbc(gridx : 1, 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))
|
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))
|
excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6))
|
||||||
|
label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7))
|
||||||
|
showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7))
|
||||||
}
|
}
|
||||||
buttonsPanel = builder.panel {
|
buttonsPanel = builder.panel {
|
||||||
gridBagLayout()
|
gridBagLayout()
|
||||||
|
@@ -4,10 +4,13 @@ import griffon.core.artifact.GriffonView
|
|||||||
import griffon.core.mvc.MVCGroup
|
import griffon.core.mvc.MVCGroup
|
||||||
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.DataHelper
|
import net.i2p.data.DataHelper
|
||||||
|
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JLabel
|
import javax.swing.JLabel
|
||||||
|
import javax.swing.JMenuItem
|
||||||
|
import javax.swing.JPopupMenu
|
||||||
import javax.swing.JTable
|
import javax.swing.JTable
|
||||||
import javax.swing.ListSelectionModel
|
import javax.swing.ListSelectionModel
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.SwingConstants
|
||||||
@@ -17,6 +20,8 @@ import com.muwire.core.util.DataUtil
|
|||||||
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
|
import java.awt.Toolkit
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
import java.awt.event.MouseAdapter
|
import java.awt.event.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
|
|
||||||
@@ -99,12 +104,27 @@ class SearchTabView {
|
|||||||
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
resultsTable.rowSorter.addRowSorterListener({ evt -> lastSortEvent = evt})
|
||||||
resultsTable.rowSorter.setSortsOnUpdates(true)
|
resultsTable.rowSorter.setSortsOnUpdates(true)
|
||||||
|
|
||||||
|
|
||||||
|
JPopupMenu menu = new JPopupMenu()
|
||||||
|
JMenuItem download = new JMenuItem("Download")
|
||||||
|
download.addActionListener({mvcGroup.parentGroup.controller.download()})
|
||||||
|
menu.add(download)
|
||||||
|
JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard")
|
||||||
|
copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()})
|
||||||
|
menu.add(copyHashToClipboard)
|
||||||
resultsTable.addMouseListener(new MouseAdapter() {
|
resultsTable.addMouseListener(new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void mouseClicked(MouseEvent e) {
|
public void mouseClicked(MouseEvent e) {
|
||||||
if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
|
if (e.button == MouseEvent.BUTTON3)
|
||||||
|
showPopupMenu(menu, e)
|
||||||
|
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
|
||||||
mvcGroup.parentGroup.controller.download()
|
mvcGroup.parentGroup.controller.download()
|
||||||
}
|
}
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e) {
|
||||||
|
if (e.button == MouseEvent.BUTTON3)
|
||||||
|
showPopupMenu(menu, e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,4 +134,21 @@ class SearchTabView {
|
|||||||
mvcGroup.parentGroup.model.searchButtonsEnabled = false
|
mvcGroup.parentGroup.model.searchButtonsEnabled = false
|
||||||
mvcGroup.destroy()
|
mvcGroup.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def showPopupMenu(JPopupMenu menu, MouseEvent e) {
|
||||||
|
println "showing popup menu"
|
||||||
|
menu.show(e.getComponent(), e.getX(), e.getY())
|
||||||
|
}
|
||||||
|
|
||||||
|
def copyHashToClipboard() {
|
||||||
|
int selected = resultsTable.getSelectedRow()
|
||||||
|
if (selected < 0)
|
||||||
|
return
|
||||||
|
if (lastSortEvent != null)
|
||||||
|
selected = resultsTable.rowSorter.convertRowIndexToModel(selected)
|
||||||
|
String hash = Base64.encode(model.results[selected].infohash.getRoot())
|
||||||
|
StringSelection selection = new StringSelection(hash)
|
||||||
|
def clipboard = Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||||
|
clipboard.setContents(selection, null)
|
||||||
|
}
|
||||||
}
|
}
|
@@ -8,6 +8,7 @@ class UISettings {
|
|||||||
boolean clearCancelledDownloads
|
boolean clearCancelledDownloads
|
||||||
boolean clearFinishedDownloads
|
boolean clearFinishedDownloads
|
||||||
boolean excludeLocalResult
|
boolean excludeLocalResult
|
||||||
|
boolean showSearchHashes
|
||||||
|
|
||||||
UISettings(Properties props) {
|
UISettings(Properties props) {
|
||||||
lnf = props.getProperty("lnf", "system")
|
lnf = props.getProperty("lnf", "system")
|
||||||
@@ -16,6 +17,7 @@ class UISettings {
|
|||||||
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
|
clearCancelledDownloads = Boolean.parseBoolean(props.getProperty("clearCancelledDownloads","false"))
|
||||||
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
|
clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false"))
|
||||||
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
|
excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","false"))
|
||||||
|
showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","false"))
|
||||||
}
|
}
|
||||||
|
|
||||||
void write(OutputStream out) throws IOException {
|
void write(OutputStream out) throws IOException {
|
||||||
@@ -25,6 +27,7 @@ class UISettings {
|
|||||||
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
props.setProperty("clearCancelledDownloads", String.valueOf(clearCancelledDownloads))
|
||||||
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads))
|
||||||
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
|
props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult))
|
||||||
|
props.setProperty("showSearchHashes", String.valueOf(showSearchHashes))
|
||||||
if (font != null)
|
if (font != null)
|
||||||
props.setProperty("font", font)
|
props.setProperty("font", font)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user