Compare commits

..

1 Commits

Author SHA1 Message Date
Zlatin Balevsky
7718dc0821 force UTF8 for json serialization 2019-06-16 12:13:00 +01:00
31 changed files with 162 additions and 555 deletions

View File

@@ -4,7 +4,7 @@ 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. It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.2.5 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 ### Building
@@ -23,7 +23,7 @@ Some of the UI tests will fail because they haven't been written yet :-/
### Running ### Running
You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.port=<port>" in there. You need to have an I2P router up and running on the same machine. After you build the application, look inside "gui/build/distributions". Untar/unzip one of the "shadow" files and then run the jar contained inside by typing "java -jar MuWire-x.y.z.jar" in a terminal or command prompt. If you use a custom I2CP host and port, create a file $HOME/.MuWire/i2p.properties and put "i2cp.tcp.host=<host>" and "i2cp.tcp.post=<port>" in there.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P. The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.

View File

@@ -32,10 +32,6 @@ For ease of deployment for new users, and so that users do not need to run a sep
Basically any non-gui non-cli user interface Basically any non-gui non-cli user interface
##### Metadata editing and search
To enable parsing of metadata from known file types and the user editing it or adding manual metadata
### Small Items ### Small Items
* Detect if router is dead and show warning or exit * Detect if router is dead and show warning or exit
@@ -43,3 +39,4 @@ To enable parsing of metadata from known file types and the user editing it or a
* Download file sequentially * Download file sequentially
* Unsharing of files * Unsharing of files
* Multiple-selection download, Ctrl-A * Multiple-selection download, Ctrl-A
* Automatic sharing of new files in shared directories (more like medium item)

View File

@@ -4,7 +4,6 @@ import java.util.concurrent.CountDownLatch
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
import com.muwire.core.UILoadedEvent
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent import com.muwire.core.connection.DisconnectionEvent
@@ -35,7 +34,7 @@ class Cli {
Core core Core core
try { try {
core = new Core(props, home, "0.2.6") core = new Core(props, home, "0.2.1")
} 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"
@@ -84,8 +83,7 @@ class Cli {
} }
core.eventBus.register(AllFilesLoadedEvent.class, fileLoader) core.eventBus.register(AllFilesLoadedEvent.class, fileLoader)
core.startServices() core.startServices()
core.eventBus.publish(new UILoadedEvent())
println "waiting for files to load" println "waiting for files to load"
latch.await() latch.await()
// now we begin // now we begin

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core Core core
try { try {
core = new Core(props, home, "0.2.6") core = new Core(props, home, "0.2.1")
} catch (Exception bad) { } catch (Exception bad) {
bad.printStackTrace(System.out) bad.printStackTrace(System.out)
println "Failed to initialize core, exiting" println "Failed to initialize core, exiting"

View File

@@ -23,7 +23,6 @@ import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.HasherService import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService import com.muwire.core.files.PersisterService
import com.muwire.core.files.DirectoryWatcher
import com.muwire.core.hostcache.CacheClient import com.muwire.core.hostcache.CacheClient
import com.muwire.core.hostcache.HostCache import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent import com.muwire.core.hostcache.HostDiscoveredEvent
@@ -71,7 +70,6 @@ public class Core {
private final ConnectionEstablisher connectionEstablisher private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService private final HasherService hasherService
private final DownloadManager downloadManager private final DownloadManager downloadManager
private final DirectoryWatcher directoryWatcher
public Core(MuWireSettings props, File home, String myVersion) { public Core(MuWireSettings props, File home, String myVersion) {
this.home = home this.home = home
@@ -164,7 +162,6 @@ public class Core {
log.info "initializing persistence service" log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager) persisterService = new PersisterService(new File(home, "files.json"), eventBus, 15000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
log.info("initializing host cache") log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json") File hostStorage = new File(home, "hosts.json")
@@ -216,9 +213,6 @@ public class Core {
connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props, connectionAcceptor = new ConnectionAcceptor(eventBus, connectionManager, props,
i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher) i2pAcceptor, hostCache, trustService, searchManager, uploadManager, connectionEstablisher)
log.info("initializing directory watcher")
directoryWatcher = new DirectoryWatcher(eventBus, fileManager)
eventBus.register(FileSharedEvent.class, directoryWatcher)
log.info("initializing hasher service") log.info("initializing hasher service")
hasherService = new HasherService(new FileHasher(), eventBus, fileManager) hasherService = new HasherService(new FileHasher(), eventBus, fileManager)
@@ -227,9 +221,9 @@ public class Core {
public void startServices() { public void startServices() {
hasherService.start() hasherService.start()
directoryWatcher.start()
trustService.start() trustService.start()
trustService.waitForLoad() trustService.waitForLoad()
persisterService.start()
hostCache.start() hostCache.start()
connectionManager.start() connectionManager.start()
cacheClient.start() cacheClient.start()
@@ -246,8 +240,6 @@ public class Core {
connectionAcceptor.stop() connectionAcceptor.stop()
log.info("shutting down connection establisher") log.info("shutting down connection establisher")
connectionEstablisher.stop() connectionEstablisher.stop()
log.info("shutting down directory watcher")
directoryWatcher.stop()
log.info("shutting down connection manager") log.info("shutting down connection manager")
connectionManager.shutdown() connectionManager.shutdown()
} }
@@ -276,7 +268,7 @@ public class Core {
} }
} }
Core core = new Core(props, home, "0.2.6") Core core = new Core(props, home, "0.2.1")
core.startServices() core.startServices()
// ... at the end, sleep or execute script // ... at the end, sleep or execute script

View File

@@ -1,11 +1,6 @@
package com.muwire.core package com.muwire.core
import java.util.stream.Collectors
import com.muwire.core.hostcache.CrawlerResponse import com.muwire.core.hostcache.CrawlerResponse
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
class MuWireSettings { class MuWireSettings {
@@ -15,9 +10,10 @@ class MuWireSettings {
int updateCheckInterval int updateCheckInterval
String nickname String nickname
File downloadLocation File downloadLocation
String sharedFiles
CrawlerResponse crawlerResponse CrawlerResponse crawlerResponse
boolean shareDownloadedFiles boolean shareDownloadedFiles
Set<String> watchedDirectories boolean watchSharedDirectories
MuWireSettings() { MuWireSettings() {
this(new Properties()) this(new Properties())
@@ -30,16 +26,11 @@ class MuWireSettings {
nickname = props.getProperty("nickname","MuWireUser") nickname = props.getProperty("nickname","MuWireUser")
downloadLocation = new File((String)props.getProperty("downloadLocation", downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home"))) System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles")
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"))
watchedDirectories = new HashSet<>()
if (props.containsKey("watchedDirectories")) {
String[] encoded = props.getProperty("watchedDirectories").split(",")
encoded.each { watchedDirectories << DataUtil.readi18nString(Base64.decode(it)) }
}
} }
void write(OutputStream out) throws IOException { void write(OutputStream out) throws IOException {
@@ -52,14 +43,9 @@ 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 (!watchedDirectories.isEmpty()) { if (sharedFiles != null)
String encoded = watchedDirectories.stream(). props.setProperty("sharedFiles", sharedFiles)
map({Base64.encode(DataUtil.encodei18nString(it))}).
collect(Collectors.joining(","))
props.setProperty("watchedDirectories", encoded)
}
props.store(out, "") props.store(out, "")
} }

View File

@@ -17,8 +17,6 @@ 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
@@ -227,15 +225,13 @@ 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)
results[i] = ResultsParser.parse(sender, resultsUUID, json) eventBus.publish(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 {

View File

@@ -2,6 +2,7 @@ package com.muwire.core.connection
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.charset.StandardCharsets
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings import com.muwire.core.MuWireSettings
@@ -66,7 +67,7 @@ class PeerConnection extends Connection {
protected void write(Object message) { protected void write(Object message) {
byte[] payload byte[] payload
if (message instanceof Map) { if (message instanceof Map) {
payload = JsonOutput.toJson(message).bytes payload = JsonOutput.toJson(message).getBytes(StandardCharsets.UTF_8)
DataUtil.packHeader(payload.length, writeHeader) DataUtil.packHeader(payload.length, writeHeader)
log.fine "$name writing message type ${message.type} length $payload.length" log.fine "$name writing message type ${message.type} length $payload.length"
writeHeader[0] &= (byte)0x7F writeHeader[0] &= (byte)0x7F

View File

@@ -58,8 +58,6 @@ public class DownloadManager {
e.result.each { e.result.each {
destinations.add(it.sender.destination) destinations.add(it.sender.destination)
} }
destinations.addAll(e.sources)
destinations.remove(me.destination)
def downloader = new Downloader(eventBus, this, me, e.target, size, def downloader = new Downloader(eventBus, this, me, e.target, size,
infohash, pieceSize, connector, destinations, infohash, pieceSize, connector, destinations,

View File

@@ -16,7 +16,6 @@ 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 {
@@ -24,7 +23,7 @@ class DownloadSession {
private static int SAMPLES = 10 private static int SAMPLES = 10
private final String meB64 private final String meB64
private final Pieces pieces private final Pieces downloaded, claimed
private final InfoHash infoHash private final InfoHash infoHash
private final Endpoint endpoint private final Endpoint endpoint
private final File file private final File file
@@ -37,10 +36,11 @@ class DownloadSession {
private ByteBuffer mapped private ByteBuffer mapped
DownloadSession(String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file, DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) { int pieceSize, long fileLength) {
this.meB64 = meB64 this.meB64 = meB64
this.pieces = pieces this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint this.endpoint = endpoint
this.infoHash = infoHash this.infoHash = infoHash
this.file = file this.file = file
@@ -63,11 +63,20 @@ class DownloadSession {
OutputStream os = endpoint.getOutputStream() OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream() InputStream is = endpoint.getInputStream()
int piece = pieces.claim() int piece
if (piece == -1) while(true) {
return false piece = downloaded.getRandomPiece()
boolean unclaim = true if (claimed.isMarked(piece)) {
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
@@ -76,6 +85,7 @@ 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))
@@ -85,7 +95,7 @@ class DownloadSession {
if (code.startsWith("404 ")) { if (code.startsWith("404 ")) {
log.warning("file not found") log.warning("file not found")
endpoint.close() endpoint.close()
return false return
} }
if (code.startsWith("416 ")) { if (code.startsWith("416 ")) {
@@ -125,46 +135,41 @@ class DownloadSession {
} }
// start the download // start the download
FileChannel channel channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
try { StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE, mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1) byte[] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
byte[] tmp = new byte[0x1 << 13] if (mapped.remaining() < tmp.length)
while(mapped.hasRemaining()) { tmp = new byte[mapped.remaining()]
if (mapped.remaining() < tmp.length) int read = is.read(tmp)
tmp = new byte[mapped.remaining()] if (read == -1)
int read = is.read(tmp) throw new IOException()
if (read == -1) synchronized(this) {
throw new IOException() mapped.put(tmp, 0, read)
synchronized(this) {
mapped.put(tmp, 0, read) if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
if (timestamps.size() == SAMPLES) { reads.removeFirst()
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)
unclaim = false 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()
downloaded.markDownloaded(piece)
} finally { } finally {
if (unclaim) claimed.clear(piece)
pieces.unclaim(piece) try { channel?.close() } catch (IOException ignore) {}
} }
return true return true
} }

View File

@@ -4,13 +4,9 @@ import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
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
@@ -21,7 +17,6 @@ import com.muwire.core.files.FileDownloadedEvent
import groovy.util.logging.Log import groovy.util.logging.Log
import net.i2p.data.Destination import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@Log @Log
public class Downloader { public class Downloader {
@@ -39,7 +34,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 pieces private final Pieces downloaded, claimed
private final long length private final long length
private InfoHash infoHash private InfoHash infoHash
private final int pieceSize private final int pieceSize
@@ -47,15 +42,12 @@ public class Downloader {
private final Set<Destination> destinations private final Set<Destination> destinations
private final int nPieces private final int nPieces
private final File piecesFile private final File piecesFile
private final File incompleteFile
final int pieceSizePow2 final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>() private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
private volatile boolean cancelled private volatile boolean cancelled
private final AtomicBoolean eventFired = new AtomicBoolean() private volatile boolean eventFired
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,
@@ -70,7 +62,6 @@ public class Downloader {
this.connector = connector this.connector = connector
this.destinations = destinations this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces") this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.incompleteFile = new File(incompletes, file.getName()+".part")
this.pieceSizePow2 = pieceSizePow2 this.pieceSizePow2 = pieceSizePow2
this.pieceSize = 1 << pieceSizePow2 this.pieceSize = 1 << pieceSizePow2
@@ -81,7 +72,8 @@ public class Downloader {
nPieces = length / pieceSize + 1 nPieces = length / pieceSize + 1
this.nPieces = nPieces this.nPieces = nPieces
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO) downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
claimed = new Pieces(nPieces)
} }
public synchronized InfoHash getInfoHash() { public synchronized InfoHash getInfoHash() {
@@ -108,24 +100,20 @@ public class Downloader {
return return
piecesFile.eachLine { piecesFile.eachLine {
int piece = Integer.parseInt(it) int piece = Integer.parseInt(it)
pieces.markDownloaded(piece) downloaded.markDownloaded(piece)
} }
} }
void writePieces() { void writePieces() {
synchronized(piecesFile) { piecesFile.withPrintWriter { writer ->
if (piecesFileClosed) downloaded.getDownloaded().each { piece ->
return writer.println(piece)
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
writer.println(piece)
}
} }
} }
} }
public long donePieces() { public long donePieces() {
pieces.donePieces() downloaded.donePieces()
} }
@@ -148,7 +136,7 @@ public class Downloader {
allFinished &= it.currentState == WorkerState.FINISHED allFinished &= it.currentState == WorkerState.FINISHED
} }
if (allFinished) { if (allFinished) {
if (pieces.isComplete()) if (downloaded.isComplete())
return DownloadState.FINISHED return DownloadState.FINISHED
return DownloadState.FAILED return DownloadState.FAILED
} }
@@ -182,11 +170,8 @@ public class Downloader {
public void cancel() { public void cancel() {
cancelled = true cancelled = true
stop() stop()
synchronized(piecesFile) { file.delete()
piecesFileClosed = true piecesFile.delete()
piecesFile.delete()
}
incompleteFile.delete()
} }
void stop() { void stop() {
@@ -246,32 +231,23 @@ public class Downloader {
} }
currentState = WorkerState.DOWNLOADING currentState = WorkerState.DOWNLOADING
boolean requestPerformed boolean requestPerformed
while(!pieces.isComplete()) { while(!downloaded.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), pieces, getInfoHash(), endpoint, incompleteFile, pieceSize, length) currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, getInfoHash(), endpoint, file, pieceSize, length)
requestPerformed = currentSession.request() requestPerformed = currentSession.request()
if (!requestPerformed) if (!requestPerformed)
break break
successfulDestinations.add(endpoint.destination)
writePieces() writePieces()
} }
} catch (Exception bad) { } catch (Exception bad) {
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 (pieces.isComplete() && eventFired.compareAndSet(false, true)) { if (downloaded.isComplete() && !eventFired) {
synchronized(piecesFile) { piecesFile.delete()
piecesFileClosed = true eventFired = true
piecesFile.delete()
}
try {
Files.move(incompleteFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
} catch (AtomicMoveNotSupportedException e) {
Files.copy(incompleteFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
incompleteFile.delete()
}
eventBus.publish( eventBus.publish(
new FileDownloadedEvent( new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, successfulDestinations), downloadedFile : new DownloadedFile(file, getInfoHash(), pieceSizePow2, Collections.emptySet()),
downloader : Downloader.this)) downloader : Downloader.this))
} }

View File

@@ -1,7 +1,7 @@
package com.muwire.core.download package com.muwire.core.download
class Pieces { class Pieces {
private final BitSet done, claimed private final BitSet bitSet
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,53 +13,52 @@ class Pieces {
Pieces(int nPieces, float ratio) { Pieces(int nPieces, float ratio) {
this.nPieces = nPieces this.nPieces = nPieces
this.ratio = ratio this.ratio = ratio
done = new BitSet(nPieces) bitSet = new BitSet(nPieces)
claimed = new BitSet(nPieces)
} }
synchronized int claim() { synchronized int getRandomPiece() {
int claimedCardinality = claimed.cardinality() int cardinality = bitSet.cardinality()
if (claimedCardinality == nPieces) if (cardinality == nPieces)
return -1 return -1
// if fuller than ratio just do sequential // if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces > ratio) { if ( (1.0f * cardinality) / nPieces > ratio) {
int rv = claimed.nextClearBit(0) return bitSet.nextClearBit(0)
claimed.set(rv)
return rv
} }
while(true) { while(true) {
int start = random.nextInt(nPieces) int start = random.nextInt(nPieces)
if (claimed.get(start)) if (bitSet.get(start))
continue continue
claimed.set(start)
return start return start
} }
} }
synchronized def getDownloaded() { def getDownloaded() {
def rv = [] def rv = []
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) { for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
rv << i rv << i
} }
rv rv
} }
synchronized void markDownloaded(int piece) { synchronized void markDownloaded(int piece) {
done.set(piece) bitSet.set(piece)
claimed.set(piece)
} }
synchronized void unclaim(int piece) { synchronized void clear(int piece) {
claimed.clear(piece) bitSet.clear(piece)
} }
synchronized boolean isComplete() { synchronized boolean isComplete() {
done.cardinality() == nPieces bitSet.cardinality() == nPieces
}
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
} }
synchronized int donePieces() { synchronized int donePieces() {
done.cardinality() bitSet.cardinality()
} }
} }

View File

@@ -3,11 +3,8 @@ package com.muwire.core.download
import com.muwire.core.Event import com.muwire.core.Event
import com.muwire.core.search.UIResultEvent import com.muwire.core.search.UIResultEvent
import net.i2p.data.Destination
class UIDownloadEvent extends Event { class UIDownloadEvent extends Event {
UIResultEvent[] result UIResultEvent[] result
Set<Destination> sources
File target File target
} }

View File

@@ -1,137 +1,4 @@
package com.muwire.core.files package com.muwire.core.files
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.Paths
import static java.nio.file.StandardWatchEventKinds.*
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.nio.file.WatchService
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.EventBus
import com.muwire.core.SharedFile
import groovy.util.logging.Log
import net.i2p.util.SystemVersion
@Log
class DirectoryWatcher { class DirectoryWatcher {
private static final long WAIT_TIME = 1000
private static final WatchEvent.Kind[] kinds
static {
if (SystemVersion.isMac())
kinds = [ENTRY_MODIFY, ENTRY_DELETE]
else
kinds = [ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE]
}
private final EventBus eventBus
private final FileManager fileManager
private final Thread watcherThread, publisherThread
private final Map<File, Long> waitingFiles = new ConcurrentHashMap<>()
private WatchService watchService
private volatile boolean shutdown
DirectoryWatcher(EventBus eventBus, FileManager fileManager) {
this.eventBus = eventBus
this.fileManager = fileManager
this.watcherThread = new Thread({watch() } as Runnable, "directory-watcher")
watcherThread.setDaemon(true)
this.publisherThread = new Thread({publish()} as Runnable, "watched-files-publisher")
publisherThread.setDaemon(true)
}
void start() {
watchService = FileSystems.getDefault().newWatchService()
watcherThread.start()
publisherThread.start()
}
void stop() {
shutdown = true
watcherThread.interrupt()
publisherThread.interrupt()
watchService.close()
}
void onFileSharedEvent(FileSharedEvent e) {
if (!e.file.isDirectory())
return
Path path = e.file.getCanonicalFile().toPath()
path.register(watchService, kinds)
}
private void watch() {
try {
while(!shutdown) {
WatchKey key = watchService.take()
key.pollEvents().each {
switch(it.kind()) {
case ENTRY_CREATE: processCreated(key.watchable(), it.context()); break
case ENTRY_MODIFY: processModified(key.watchable(), it.context()); break
case ENTRY_DELETE: processDeleted(key.watchable(), it.context()); break
}
}
key.reset()
}
} catch (InterruptedException e) {
if (!shutdown)
throw e
}
}
private void processCreated(Path parent, Path path) {
File f= join(parent, path)
log.fine("created entry $f")
if (f.isDirectory())
f.toPath().register(watchService, kinds)
}
private void processModified(Path parent, Path path) {
File f = join(parent, path)
log.fine("modified entry $f")
waitingFiles.put(f, System.currentTimeMillis())
}
private void processDeleted(Path parent, Path path) {
File f = join(parent, path)
log.fine("deleted entry $f")
SharedFile sf = fileManager.fileToSharedFile.get(f)
if (sf != null)
eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
}
private static File join(Path parent, Path path) {
File parentFile = parent.toFile().getCanonicalFile()
new File(parentFile, path.toFile().getName())
}
private void publish() {
try {
while(!shutdown) {
Thread.sleep(WAIT_TIME)
long now = System.currentTimeMillis()
def published = []
waitingFiles.each { file, timestamp ->
if (now - timestamp > WAIT_TIME) {
log.fine("publishing file $file")
eventBus.publish new FileSharedEvent(file : file)
published << file
}
}
published.each {
waitingFiles.remove(it)
}
}
} catch (InterruptedException e) {
if (!shutdown)
throw e
}
}
} }

View File

@@ -32,7 +32,7 @@ class HasherService {
private void process(File f) { private void process(File f) {
f = f.getCanonicalFile() f = f.getCanonicalFile()
if (f.isDirectory()) { if (f.isDirectory()) {
f.listFiles().each {eventBus.publish new FileSharedEvent(file: it) } f.listFiles().each {onFileSharedEvent new FileSharedEvent(file: it) }
} else { } else {
if (f.length() == 0) { if (f.length() == 0) {
eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f") eventBus.publish new FileHashedEvent(error: "Not sharing empty file $f")

View File

@@ -11,7 +11,6 @@ import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Service import com.muwire.core.Service
import com.muwire.core.SharedFile import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput import groovy.json.JsonOutput
@@ -37,14 +36,14 @@ class PersisterService extends Service {
timer = new Timer("file persister", true) timer = new Timer("file persister", true)
} }
void start() {
timer.schedule({load()} as TimerTask, 1)
}
void stop() { void stop() {
timer.cancel() timer.cancel()
} }
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void load() { void load() {
if (location.exists() && location.isFile()) { if (location.exists() && location.isFile()) {
def slurper = new JsonSlurper() def slurper = new JsonSlurper()

View File

@@ -1,7 +1,5 @@
package com.muwire.core.search package com.muwire.core.search
import java.util.stream.Collectors
import javax.naming.directory.InvalidSearchControlsException import javax.naming.directory.InvalidSearchControlsException
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
@@ -9,7 +7,6 @@ import com.muwire.core.Persona
import com.muwire.core.util.DataUtil import com.muwire.core.util.DataUtil
import net.i2p.data.Base64 import net.i2p.data.Base64
import net.i2p.data.Destination
class ResultsParser { class ResultsParser {
public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException { public static UIResultEvent parse(Persona p, UUID uuid, def json) throws InvalidSearchResultException {
@@ -61,7 +58,6 @@ class ResultsParser {
size : size, size : size,
infohash : parsedIH, infohash : parsedIH,
pieceSize : pieceSize, pieceSize : pieceSize,
sources : Collections.emptySet(),
uuid : uuid) uuid : uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)
@@ -86,17 +82,11 @@ class ResultsParser {
if (infoHash.length != InfoHash.SIZE) if (infoHash.length != InfoHash.SIZE)
throw new InvalidSearchResultException("invalid infohash size $infoHash.length") throw new InvalidSearchResultException("invalid infohash size $infoHash.length")
int pieceSize = json.pieceSize int pieceSize = json.pieceSize
Set<Destination> sources = Collections.emptySet()
if (json.sources != null)
sources = json.sources.stream().map({new Destination(it)}).collect(Collectors.toSet())
return new UIResultEvent( sender : p, return new UIResultEvent( sender : p,
name : name, name : name,
size : size, size : size,
infohash : new InfoHash(infoHash), infohash : new InfoHash(infoHash),
pieceSize : pieceSize, pieceSize : pieceSize,
sources : sources,
uuid: uuid) uuid: uuid)
} catch (Exception e) { } catch (Exception e) {
throw new InvalidSearchResultException("parsing search result failed",e) throw new InvalidSearchResultException("parsing search result failed",e)

View File

@@ -11,9 +11,7 @@ import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.stream.Collectors
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus import com.muwire.core.EventBus
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
@@ -56,16 +54,12 @@ class ResultsSender {
int pieceSize = it.getPieceSize() int pieceSize = it.getPieceSize()
if (pieceSize == 0) if (pieceSize == 0)
pieceSize = FileHasher.getPieceSize(length) pieceSize = FileHasher.getPieceSize(length)
Set<Destination> suggested = Collections.emptySet()
if (it instanceof DownloadedFile)
suggested = it.sources
def uiResultEvent = new UIResultEvent( sender : me, def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(), name : it.getFile().getName(),
size : length, size : length,
infohash : it.getInfoHash(), infohash : it.getInfoHash(),
pieceSize : pieceSize, pieceSize : pieceSize,
uuid : uuid, uuid : uuid
sources : suggested
) )
eventBus.publish(uiResultEvent) eventBus.publish(uiResultEvent)
} }
@@ -116,10 +110,6 @@ class ResultsSender {
} }
obj.hashList = hashListB64 obj.hashList = hashListB64
} }
if (it instanceof DownloadedFile)
obj.sources = it.sources.stream().map({dest -> dest.toBase64()}).collect(Collectors.toSet())
def json = jsonOutput.toJson(obj) def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length()) os.writeShort((short)json.length())
os.write(json.getBytes(StandardCharsets.US_ASCII)) os.write(json.getBytes(StandardCharsets.US_ASCII))

View File

@@ -1,8 +0,0 @@
package com.muwire.core.search
import com.muwire.core.Event
class UIResultBatchEvent extends Event {
UUID uuid
UIResultEvent[] results
}

View File

@@ -4,11 +4,8 @@ import com.muwire.core.Event
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.Persona import com.muwire.core.Persona
import net.i2p.data.Destination
class UIResultEvent extends Event { class UIResultEvent extends Event {
Persona sender Persona sender
Set<Destination> sources
UUID uuid UUID uuid
String name String name
long size long size

View File

@@ -51,23 +51,4 @@ 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()
}
} }

View File

@@ -6,8 +6,6 @@ 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
@@ -16,7 +14,6 @@ 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() {
@@ -35,21 +32,4 @@ 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()
}
} }

View File

@@ -23,13 +23,4 @@ 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();
} }

View File

@@ -25,17 +25,4 @@ 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);
}
} }

View File

@@ -1,5 +1,5 @@
group = com.muwire group = com.muwire
version = 0.2.6 version = 0.2.1
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

View File

@@ -62,7 +62,7 @@ class MainFrameController {
def searchEvent def searchEvent
if (hashSearch) { if (hashSearch) {
searchEvent = new SearchEvent(searchHash : root, uuid : uuid, oobInfohash: true) searchEvent = new SearchEvent(searchHash : root, uuid : uuid)
} 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, " ")
@@ -118,20 +118,16 @@ class MainFrameController {
void download() { void download() {
def result = selectedResult() def result = selectedResult()
if (result == null) if (result == null)
return return // TODO disable button
if (!model.canDownload(result.infohash))
return
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name) def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
def selected = builder.getVariable("result-tabs").getSelectedComponent() def selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group") def group = selected.getClientProperty("mvc-group")
def resultsBucket = group.model.hashBucket[result.infohash] def resultsBucket = group.model.hashBucket[result.infohash]
def sources = group.model.sourcesBucket[result.infohash]
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, sources: sources, target : file)) core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file))
} }
@ControllerAction @ControllerAction
@@ -154,7 +150,6 @@ class MainFrameController {
void cancel() { void cancel() {
def downloader = model.downloads[selectedDownload()].downloader def downloader = model.downloads[selectedDownload()].downloader
downloader.cancel() downloader.cancel()
model.downloadInfoHashes.remove(downloader.getInfoHash())
core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader)) core.eventBus.publish(new UIDownloadCancelledEvent(downloader : downloader))
} }
@@ -195,13 +190,6 @@ class MainFrameController {
println "unsharing selected files" println "unsharing selected files"
} }
void saveMuWireSettings() {
File f = new File(core.home, "MuWire.properties")
f.withOutputStream {
core.muOptions.write(it)
}
}
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e-> application.addPropertyChangeListener("core", {e->
core = e.getNewValue() core = e.getNewValue()

View File

@@ -1,4 +1,3 @@
import griffon.core.GriffonApplication import griffon.core.GriffonApplication
import griffon.core.env.Metadata import griffon.core.env.Metadata
import groovy.util.logging.Log import groovy.util.logging.Log
@@ -105,6 +104,12 @@ class Ready extends AbstractLifecycleHandler {
it.propertyChange(new PropertyChangeEvent(this, "core", null, core)) it.propertyChange(new PropertyChangeEvent(this, "core", null, core))
} }
if (props.sharedFiles != null) {
props.sharedFiles.split(",").each {
core.eventBus.publish(new FileSharedEvent(file : new File(it)))
}
}
core.eventBus.publish(new UILoadedEvent()) core.eventBus.publish(new UILoadedEvent())
} }
} }

View File

@@ -9,7 +9,6 @@ import javax.swing.JTable
import com.muwire.core.Core import com.muwire.core.Core
import com.muwire.core.InfoHash import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.Persona import com.muwire.core.Persona
import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.ConnectionEvent
@@ -20,9 +19,7 @@ import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent 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.files.FileUnsharedEvent
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
@@ -55,7 +52,6 @@ class MainFrameModel {
def downloads = [] def downloads = []
def uploads = [] def uploads = []
def shared = [] def shared = []
def watched = []
def connectionList = [] def connectionList = []
def searches = new LinkedList() def searches = new LinkedList()
def trusted = [] def trusted = []
@@ -63,14 +59,11 @@ class MainFrameModel {
@Observable int connections @Observable int connections
@Observable String me @Observable String me
@Observable boolean downloadActionEnabled @Observable boolean searchButtonsEnabled
@Observable boolean trustButtonsEnabled
@Observable boolean cancelButtonEnabled @Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled @Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>() private final Set<InfoHash> infoHashes = new HashSet<>()
private final Set<InfoHash> downloadInfoHashes = new HashSet<>()
volatile Core core volatile Core core
@@ -122,7 +115,6 @@ 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)
@@ -134,10 +126,9 @@ class MainFrameModel {
core.eventBus.register(QueryEvent.class, this) core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this) core.eventBus.register(UpdateAvailableEvent.class, this)
core.eventBus.register(FileDownloadedEvent.class, this) core.eventBus.register(FileDownloadedEvent.class, this)
core.eventBus.register(FileUnsharedEvent.class, this)
timer.schedule({ timer.schedule({
int retryInterval = core.muOptions.downloadRetryInterval int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) { if (retryInterval > 0) {
retryInterval *= 60000 retryInterval *= 60000
long now = System.currentTimeMillis() long now = System.currentTimeMillis()
@@ -160,10 +151,6 @@ class MainFrameModel {
runInsideUIAsync { runInsideUIAsync {
trusted.addAll(core.trustService.good.values()) trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values()) distrusted.addAll(core.trustService.bad.values())
watched.addAll(core.muOptions.watchedDirectories)
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
watched.each { core.eventBus.publish(new FileSharedEvent(file : new File(it))) }
} }
}) })
@@ -174,15 +161,9 @@ 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
downloadInfoHashes.add(e.downloader.infoHash)
} }
} }
@@ -244,17 +225,6 @@ class MainFrameModel {
} }
} }
void onFileUnsharedEvent(FileUnsharedEvent e) {
InfoHash infohash = e.unsharedFile.infoHash
if (!infoHashes.remove(infohash))
return
runInsideUIAsync {
shared.remove(e.unsharedFile)
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
}
}
void onUploadEvent(UploadEvent e) { void onUploadEvent(UploadEvent e) {
runInsideUIAsync { runInsideUIAsync {
uploads << e.uploader uploads << e.uploader
@@ -363,8 +333,4 @@ class MainFrameModel {
return destination == other.destination return destination == other.destination
} }
} }
boolean canDownload(InfoHash hash) {
!downloadInfoHashes.contains(hash)
}
} }

View File

@@ -23,7 +23,6 @@ class SearchTabModel {
String uuid String uuid
def results = [] def results = []
def hashBucket = [:] def hashBucket = [:]
def sourcesBucket = [:]
void mvcGroupInit(Map<String, String> args) { void mvcGroupInit(Map<String, String> args) {
@@ -47,43 +46,10 @@ class SearchTabModel {
hashBucket[e.infohash] = bucket hashBucket[e.infohash] = bucket
} }
bucket << e bucket << e
Set sourceBucket = sourcesBucket.get(e.infohash)
if (sourceBucket == null) {
sourceBucket = new HashSet()
sourcesBucket.put(e.infohash, sourceBucket)
}
sourceBucket.addAll(e.sources)
results << e results << e
JTable table = builder.getVariable("results-table") JTable table = builder.getVariable("results-table")
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
}
Set sourceBucket = sourcesBucket.get(it.infohash)
if (sourceBucket == null) {
sourceBucket = new HashSet()
sourcesBucket.put(it.infohash, sourceBucket)
}
sourceBucket.addAll(it.sources)
bucket << it
results << it
}
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()
}
}
} }

View File

@@ -105,9 +105,9 @@ class MainFrameView {
borderLayout() borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER) tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) { panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", enabled : bind {model.downloadActionEnabled}, downloadAction) button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.trustButtonsEnabled }, trustAction) button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.trustButtonsEnabled}, distrustAction) button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
} }
} }
panel (constraints : JSplitPane.BOTTOM) { panel (constraints : JSplitPane.BOTTOM) {
@@ -120,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".toString() "$done/$pieces pieces"
}) })
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 ->
@@ -139,32 +139,16 @@ class MainFrameView {
panel (constraints: "uploads window"){ panel (constraints: "uploads window"){
gridLayout(cols : 1, rows : 2) gridLayout(cols : 1, rows : 2)
panel { panel {
gridLayout(cols : 2, rows : 1) borderLayout()
panel { panel (constraints : BorderLayout.NORTH) {
borderLayout() button(text : "Click here to share files", actionPerformed : shareFiles)
panel (constraints : BorderLayout.NORTH) {
button(text : "Add directories to watch", actionPerformed : watchDirectories)
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "watched-directories-table", autoCreateRowSorter: true) {
tableModel(list : model.watched) {
closureColumn(header: "Watched Directories", type : String, read : { it })
}
}
}
} }
panel { scrollPane ( constraints : BorderLayout.CENTER) {
borderLayout() table(id : "shared-files-table", autoCreateRowSorter: true) {
panel (constraints : BorderLayout.NORTH) { tableModel(list : model.shared) {
button(text : "Share files", actionPerformed : shareFiles) closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
} closureColumn(header : "Size", preferredWidth : 50, type : Long, read : {row -> row.file.length() })
scrollPane(constraints : BorderLayout.CENTER) { }
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.file.length() })
}
}
} }
} }
} }
@@ -176,13 +160,16 @@ 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.getName() }) closureColumn(header : "Name", type : String, read : {row -> row.file.getName() })
closureColumn(header : "Progress", type : String, read : { row -> closureColumn(header : "Progress", type : String, read : { row ->
int percent = row.getProgress() int position = row.getPosition()
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.getDownloader() row.request.downloader?.getHumanReadableName()
}) })
} }
} }
@@ -482,26 +469,11 @@ class MainFrameView {
def shareFiles = { def shareFiles = {
def chooser = new JFileChooser() def chooser = new JFileChooser()
chooser.setDialogTitle("Select file to share") chooser.setDialogTitle("Select file or directory to share")
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY) chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES)
int rv = chooser.showOpenDialog(null) int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) { if (rv == JFileChooser.APPROVE_OPTION) {
model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile())) model.core.eventBus.publish(new FileSharedEvent(file : chooser.getSelectedFile()))
} }
} }
def watchDirectories = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select directory to watch")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
int rv = chooser.showOpenDialog(null)
if (rv == JFileChooser.APPROVE_OPTION) {
File f = chooser.getSelectedFile()
model.watched << f.getAbsolutePath()
application.context.get("muwire-settings").watchedDirectories << f.getAbsolutePath()
mvcGroup.controller.saveMuWireSettings()
builder.getVariable("watched-directories-table").model.fireTableDataChanged()
model.core.eventBus.publish(new FileSharedEvent(file : f))
}
}
} }

View File

@@ -47,9 +47,8 @@ class SearchTabView {
resultsTable = table(id : "results-table", autoCreateRowSorter : true) { resultsTable = table(id : "results-table", autoCreateRowSorter : true) {
tableModel(list: model.results) { tableModel(list: model.results) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')}) closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name.replace('<','_')})
closureColumn(header: "Size", preferredWidth: 20, type: Long, read : {row -> row.size}) closureColumn(header: "Size", preferredWidth: 50, type: Long, read : {row -> row.size})
closureColumn(header: "Direct Sources", preferredWidth: 50, type : Integer, read : { row -> model.hashBucket[row.infohash].size()}) closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Possible Sources", preferredWidth : 50, type : Integer, read : {row -> model.sourcesBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()}) closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row -> closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination).toString() model.core.trustService.getLevel(row.sender.destination).toString()
@@ -67,13 +66,7 @@ class SearchTabView {
def selectionModel = resultsTable.getSelectionModel() def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( { selectionModel.addListSelectionListener( {
int row = resultsTable.getSelectedRow() mvcGroup.parentGroup.model.searchButtonsEnabled = true
if (row < 0)
return
if (lastSortEvent != null)
row = resultsTable.rowSorter.convertRowIndexToModel(row)
mvcGroup.parentGroup.model.trustButtonsEnabled = true
mvcGroup.parentGroup.model.downloadActionEnabled = mvcGroup.parentGroup.model.canDownload(model.results[row].infohash)
}) })
} }
} }
@@ -112,18 +105,25 @@ class SearchTabView {
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.BUTTON3) if (e.button == MouseEvent.BUTTON3)
showPopupMenu(e) showPopupMenu(menu, e)
else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2) else if (e.button == MouseEvent.BUTTON1 && e.clickCount == 2)
mvcGroup.parentGroup.controller.download() mvcGroup.parentGroup.controller.download()
} }
@Override @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (e.button == MouseEvent.BUTTON3) if (e.button == MouseEvent.BUTTON3)
showPopupMenu(e) showPopupMenu(menu, e)
} }
}) })
} }
@@ -131,21 +131,12 @@ class SearchTabView {
def closeTab = { def closeTab = {
int index = parent.indexOfTab(searchTerms) int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index) parent.removeTabAt(index)
mvcGroup.parentGroup.model.trustButtonsEnabled = false mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.parentGroup.model.downloadActionEnabled = false
mvcGroup.destroy() mvcGroup.destroy()
} }
def showPopupMenu(MouseEvent e) { def showPopupMenu(JPopupMenu menu, MouseEvent e) {
JPopupMenu menu = new JPopupMenu() println "showing popup menu"
if (mvcGroup.parentGroup.model.downloadActionEnabled) {
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)
menu.show(e.getComponent(), e.getX(), e.getY()) menu.show(e.getComponent(), e.getX(), e.getY())
} }