Compare commits

..

32 Commits

Author SHA1 Message Date
853b9f67fc Release 0.6.10 2020-02-23 15:42:03 +00:00
a505a2449a persist SharedFile on change of comments #35 2020-02-18 02:14:32 +00:00
c11d81c6c3 Release 0.6.9 2020-02-16 16:33:33 +00:00
ee5e90c4ab ignore events from old persister service, prevents duplicate entries during migration #35 2020-02-14 18:20:39 +00:00
64d2a87d26 more occurrences of SharedFile::getInfoHash #35 2020-02-14 17:53:09 +00:00
f0304dbe7d fix copy-hash-to-clipboard #35 2020-02-14 16:14:36 +00:00
bdad8d9309 make extended signatures mandatory 2020-02-14 15:34:21 +00:00
8c110bbae5 more occurrences of SharedFile::getInfoHash #35 2020-02-14 15:24:39 +00:00
2cc1e384bc more occurrences of SharedFile::getInfoHash #35 2020-02-14 15:20:01 +00:00
9337d1b74d chase down references to missing infoHash #35 2020-02-14 01:48:02 +00:00
16ed5dd346 chase down some usages of deprecated getInfoHash method #35 2020-02-14 01:32:38 +00:00
7b55fc9ed8 working uploads #35 2020-02-14 01:15:10 +00:00
d5c8050572 wip on separate hashlist storage #35 2020-02-14 00:37:07 +00:00
83546d68d2 Merge pull request #37 from LoveIsGrief/change-persister
Introduce persister that uses a directory structure
2020-01-25 14:36:41 +00:00
a891c83518 Only persist downloaded files if sharing thereof is enabled
Otherwise we might inadvertently share downloads
2020-01-25 15:25:48 +01:00
aa56cc23c0 Cache base 64 path hash
Can't do it in constructor without an ugly try/catch
 therefore this is done on demand
2020-01-25 15:20:38 +01:00
a2b37ef567 Persist downloaded files 2020-01-25 15:06:12 +01:00
4bc04ae631 Revert "Reduce log levels in Connection"
This reverts commit dcd233b7
2020-01-25 15:01:21 +01:00
56da9a16b0 Set FileLoadedEvent::source in the subclass
Setting it in the super class means we don't set the right value for every case
2020-01-25 15:00:48 +01:00
2935ee1a1d Remove unnecessary executor
It was doing nothing but starting and stopping
2020-01-25 14:49:59 +01:00
855183397b Remove TODO
There's already an issue open https://github.com/zlatinb/muwire/issues/35
2020-01-22 21:35:54 +01:00
e27704c1af Make sure migration from PersisterService works
this.getClass() and this.class kept resolving to Class.
Using a string is much simpler

mkdirs() is also necessary because the directory structure doesn't exist
 when persistFile is called the first time
2020-01-22 20:59:05 +01:00
5c18b4a141 Add more logs PersisterFolderService 2020-01-22 15:12:22 +01:00
dcd233b7ad Reduce log levels in Connection
Too verbose
2020-01-22 15:12:01 +01:00
7cee8a28ba FileLoadedEvent should include class when coming from old persister
Otherwise the new PersisterFolderService won't migrate
2020-01-22 15:07:00 +01:00
7446fc949a Remove UIPersistFilesEvent
Hashing is done per file now and those are triggered by individual events
2020-01-22 13:00:55 +01:00
598ab90f63 Clear up the event path when starting up the old and new persisters
The new persister won't load anything until the old one has finished
2020-01-22 12:36:34 +01:00
043028c296 Introduce PersisterFolderService to replace PersisterService
An attempt at automatically migrate from PersisterService was made, but the events aren't triggered in the right order.
We need to make sure that we don't trigger the "AllFilesLoadedEvent" before the migration is done
2020-01-21 23:34:33 +01:00
cd1757fac3 Use Java 11
Java9 isn't available on Ubuntu anymore, which would make development harder
2020-01-19 21:46:47 +01:00
9d4b365e63 Log the time it take to persist files and hashes 2020-01-19 21:43:03 +01:00
b12d57e30a fix bracket 2020-01-14 20:27:21 +00:00
f33d1b6db3 move the docker documentation to the wiki 2020-01-14 20:26:47 +00:00
32 changed files with 450 additions and 269 deletions

View File

@ -4,7 +4,7 @@ FROM jlesage/baseimage-gui:alpine-3.10-glibc
ARG DOCKER_IMAGE_VERSION=unknown
# JDK version
ARG JDK=9
ARG JDK=11
# Important directories
ARG TMP_DIR=/muwire-tmp

View File

@ -44,53 +44,7 @@ There is a Web-based UI under development. It is intended to be run as a plugin
## Docker
The Docker image is based on the wonderful work in [jlesage/docker-baseimage-gui].
You can refer to it for environment variables to pass to the container.
If you don't want to use the image on dockerhub, build an image yourself.
```bash
MUWIRE_VERSION=`awk -F "=" '/^version/ { gsub(" ","") ; print $2}' gradle.properties`
docker build -t muwire:latest,muwire:${MUWIRE_VERSION} .
```
**Necessary configuration**
Since MuWire will be running in a container, it won't have direct access to the host's localhost.
By default, it will be configured to use `172.17.0.1` as the target host.
You'll need to open the I2CP port on that interface.
If you're running I2P on the localhost, navigate to http://localhost:7657/configi2cp and make the necessary changes.
![i2cp_config.png]
Should you be using a different interface write an `i2p.properties` and then put that into the shared docker volume.
Example configuration file:
```properties
i2cp.tcp.host=112.13.0.1
```
**Running**
```bash
docker run \
-p 5800:5800 \
-v config:/muwire/.MuWire \
-v incompletes:/incompletes \
-v output:/output \
--name muwire \
zlatinb/muwire
```
You will then be able to access the muwire GUI over a browser at http://localhost:5800
**Options**
| Option | Description |
|--------------|--------------------------------------------|
|`-v config:/muwire/.MuWire`| This is where the `i2p.properties` and possibly other config should go |
|`-v incompletes:/incompletes`| The `/incompletes` volume should be used to store MuWire's **incomplete** download/upload data \*|
|`-v output:/output`| The `/output` volume should be used to store MuWire's download/upload data |
MuWire is available as a Docker image. For more information see the [Docker] page.
## Translations
If you want to help translate MuWire, instructions are on the wiki https://github.com/zlatinb/muwire/wiki/Translate
@ -111,6 +65,5 @@ You can find the full key at https://keybase.io/zlatinb
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
[I2P Github]: https://github.com/i2p/i2p.i2p
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin
[i2cp_config.png]: ./images/i2cp_config.png
[muwire_incompletes.png]: ./images/muwire_incompletes.png
[Docker]: https://github.com/zlatinb/muwire/wiki/Docker
[jlesage/docker-baseimage-gui]: https://github.com/jlesage/docker-baseimage-gui

View File

@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.6.8"
private static final String MW_VERSION = "0.6.10"
private static volatile Core core

View File

@ -3,6 +3,7 @@ package com.muwire.clilanterna
import com.googlecode.lanterna.gui2.TextGUIThread
import com.googlecode.lanterna.gui2.table.TableModel
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryWatchedEvent
@ -72,7 +73,7 @@ class FilesModel {
sharedFiles.each {
long size = it.getCachedLength()
boolean comment = it.comment != null
boolean certified = core.certificateManager.hasLocalCertificate(it.getInfoHash())
boolean certified = core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
String hits = String.valueOf(it.getHits())
String downloaders = String.valueOf(it.getDownloaders().size())
model.addRow(new SharedFileWrapper(it), DataHelper.formatSize2(size, false)+"B", comment, certified, hits, downloaders)

View File

@ -21,7 +21,6 @@ import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
class FilesView extends BasicWindow {
private final FilesModel model
@ -84,7 +83,6 @@ class FilesView extends BasicWindow {
Button unshareButton = new Button("Unshare", {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : sf))
core.eventBus.publish(new UIPersistFilesEvent())
MessageDialog.showMessageDialog(textGUI, "File Unshared", "Unshared "+sf.getFile().getName(), MessageDialogButton.OK)
} )
Button addCommentButton = new Button("Add Comment", {

View File

@ -1,5 +1,8 @@
package com.muwire.core
import com.muwire.core.files.PersisterDoneEvent
import com.muwire.core.files.PersisterFolderService
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
@ -31,7 +34,6 @@ import com.muwire.core.filecert.UIFetchCertificatesEvent
import com.muwire.core.filecert.UIImportCertificateEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileManager
@ -41,7 +43,7 @@ import com.muwire.core.files.HasherService
import com.muwire.core.files.PersisterService
import com.muwire.core.files.SideCarFileEvent
import com.muwire.core.files.UICommentEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.DirectoryWatchedEvent
@ -74,10 +76,8 @@ import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.streaming.I2PSocketManager
import net.i2p.client.streaming.I2PSocketManagerFactory
import net.i2p.client.streaming.I2PSocketOptions
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener
import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType
import net.i2p.data.Destination
import net.i2p.data.PrivateKey
import net.i2p.data.Signature
@ -100,6 +100,7 @@ public class Core {
final TrustService trustService
final TrustSubscriber trustSubscriber
private final PersisterService persisterService
private final PersisterFolderService persisterFolderService
private final HostCache hostCache
private final ConnectionManager connectionManager
private final CacheClient cacheClient
@ -259,7 +260,15 @@ public class Core {
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
eventBus.register(UILoadedEvent.class, persisterService)
eventBus.register(UIPersistFilesEvent.class, persisterService)
log.info "initializing folder persistence service"
persisterFolderService = new PersisterFolderService(this, new File(home, "files"), eventBus)
eventBus.register(PersisterDoneEvent.class, persisterFolderService)
eventBus.register(FileDownloadedEvent.class, persisterFolderService)
eventBus.register(FileLoadedEvent.class, persisterFolderService)
eventBus.register(FileHashedEvent.class, persisterFolderService)
eventBus.register(FileUnsharedEvent.class, persisterFolderService)
eventBus.register(UICommentEvent.class, persisterFolderService)
log.info("initializing host cache")
File hostStorage = new File(home, "hosts.json")
@ -321,7 +330,7 @@ public class Core {
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, props)
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
log.info("initializing connection establisher")
connectionEstablisher = new ConnectionEstablisher(eventBus, i2pConnector, props, connectionManager, hostCache)
@ -398,6 +407,8 @@ public class Core {
trustService.stop()
log.info("shutting down persister service")
persisterService.stop()
log.info("shutting down persisterFolder service")
persisterFolderService.stop()
log.info("shutting down download manager")
downloadManager.shutdown()
log.info("shutting down connection acceptor")
@ -459,7 +470,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.6.8")
Core core = new Core(props, home, "0.6.10")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -255,7 +255,6 @@ abstract class Connection implements Closeable {
return
}
// TODO: make this mandatory at some point
byte[] sig2 = null
long queryTime = 0
if (search.sig2 != null) {
@ -278,8 +277,10 @@ abstract class Connection implements Closeable {
return
}
}
} else
} else {
log.info("no extended signature in query")
return
}
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : infohash,

View File

@ -380,7 +380,7 @@ class ConnectionAcceptor {
JsonOutput jsonOutput = new JsonOutput()
sharedFiles.each {
it.hit(browser, System.currentTimeMillis(), "Browse Host");
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = ResultsSender.sharedFileToObj(it, false, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())

View File

@ -405,8 +405,9 @@ public class Downloader {
}
eventBus.publish(
new FileDownloadedEvent(
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this))
downloadedFile : new DownloadedFile(file.getCanonicalFile(), getInfoHash().getRoot(), pieceSizePow2, successfulDestinations),
downloader : Downloader.this,
infoHash: getInfoHash()))
}
endpoint?.close()

View File

@ -70,7 +70,7 @@ class CertificateManager {
}
void onUICreateCertificateEvent(UICreateCertificateEvent e) {
InfoHash infoHash = e.sharedFile.getInfoHash()
InfoHash infoHash = new InfoHash(e.sharedFile.getRoot())
String name = e.sharedFile.getFile().getName()
long timestamp = System.currentTimeMillis()

View File

@ -0,0 +1,152 @@
package com.muwire.core.files
import com.muwire.core.DownloadedFile
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Service
import com.muwire.core.SharedFile
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.data.Destination
import java.util.stream.Collectors
abstract class BasePersisterService extends Service{
protected static FileLoadedEvent fromJson(def json) {
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
throw new IllegalArgumentException()
if (!(json.hashList instanceof List))
throw new IllegalArgumentException()
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
file = file.getCanonicalFile()
if (!file.exists() || file.isDirectory())
return null
long length = Long.valueOf(json.length)
if (length != file.length())
return null
List hashList = (List) json.hashList
ByteArrayOutputStream baos = new ByteArrayOutputStream()
hashList.each {
byte [] hash = Base64.decode it.toString()
if (hash == null)
throw new IllegalArgumentException()
baos.write hash
}
byte[] hashListBytes = baos.toByteArray()
InfoHash ih = InfoHash.fromHashList(hashListBytes)
byte [] root = Base64.decode(json.infoHash.toString())
if (root == null)
throw new IllegalArgumentException()
if (!Arrays.equals(root, ih.getRoot()))
return null
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
}
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
sf.setComment(json.comment)
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
}
protected static FileLoadedEvent fromJsonLite(json) {
if (json.file == null || json.length == null || json.root == null)
throw new IllegalArgumentException()
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
file = file.getCanonicalFile()
if (!file.exists() || file.isDirectory())
return null
long length = Long.valueOf(json.length)
if (length != file.length())
return null
byte[] root = Base64.decode(json.root)
InfoHash ih = new InfoHash(root)
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({ d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih.getRoot(), pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df, infoHash: ih)
}
SharedFile sf = new SharedFile(file, ih.getRoot(), pieceSize)
sf.setComment(json.comment)
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf, infoHash: ih)
}
protected static toJson(SharedFile sf) {
def json = [:]
json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength()
json.root = Base64.encode(sf.getRoot())
json.pieceSize = sf.getPieceSize()
json.comment = sf.getComment()
json.hits = sf.getHits()
json.downloaders = sf.getDownloaders()
if (!sf.searches.isEmpty()) {
Set searchers = new HashSet<>()
sf.searches.each {
def search = [:]
if (it.searcher != null)
search.searcher = it.searcher.toBase64()
search.timestamp = it.timestamp
search.query = it.query
searchers.add(search)
}
json.searchers = searchers
}
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
}
json
}
}

View File

@ -2,6 +2,7 @@ package com.muwire.core.files
import com.muwire.core.DownloadedFile
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.download.Downloader
import net.i2p.data.Destination
@ -9,4 +10,5 @@ import net.i2p.data.Destination
class FileDownloadedEvent extends Event {
Downloader downloader
DownloadedFile downloadedFile
InfoHash infoHash
}

View File

@ -1,11 +1,13 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
class FileHashedEvent extends Event {
SharedFile sharedFile
InfoHash infoHash
String error
@Override

View File

@ -1,9 +1,12 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
class FileLoadedEvent extends Event {
SharedFile loadedFile
InfoHash infoHash
String source
}

View File

@ -75,7 +75,7 @@ class FileManager {
private void addToIndex(SharedFile sf) {
log.info("Adding shared file " + sf.getFile())
InfoHash infoHash = sf.getInfoHash()
InfoHash infoHash = new InfoHash(sf.getRoot())
Set<SharedFile> existing = rootToFiles.get(infoHash)
if (existing == null) {
log.info("adding new root")
@ -117,7 +117,7 @@ class FileManager {
void onFileUnsharedEvent(FileUnsharedEvent e) {
SharedFile sf = e.unsharedFile
InfoHash infoHash = sf.getInfoHash()
InfoHash infoHash = new InfoHash(sf.getRoot())
Set<SharedFile> existing = rootToFiles.get(infoHash)
if (existing != null) {
existing.remove(sf)

View File

@ -65,7 +65,8 @@ class HasherService {
} else {
eventBus.publish new FileHashingEvent(hashingFile: f)
def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash.getRoot(), FileHasher.getPieceSize(f.length())),
infoHash : hash)
}
}
}

View File

@ -0,0 +1,12 @@
package com.muwire.core.files
import com.muwire.core.Event
/**
* Should be triggered by the old PersisterService
* once it has finished reading the old file
*
* @see PersisterService
*/
class PersisterDoneEvent extends Event{
}

View File

@ -0,0 +1,180 @@
package com.muwire.core.files
import com.muwire.core.*
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level
/**
* A persister that stores information about the files shared using
* individual JSON files in directories.
*
* The absolute path's 32bit hash to the shared file is used
* to build the directory and filename.
*
* This persister only starts working once the old persister has finished loading
* @see PersisterFolderService#getJsonPath
*/
@Log
class PersisterFolderService extends BasePersisterService {
final static int CUT_LENGTH = 6
private final Core core;
final File location
final EventBus listener
final int interval
final Timer timer
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterFolderService(Core core, File location, EventBus listener) {
this.core = core;
this.location = location
this.listener = listener
this.interval = interval
timer = new Timer("file-folder persister timer", true)
}
void stop() {
timer.cancel()
persisterExecutor.shutdown()
}
void onPersisterDoneEvent(PersisterDoneEvent persisterDoneEvent) {
log.info("Old persister done")
load()
}
void onFileHashedEvent(FileHashedEvent hashedEvent) {
persistFile(hashedEvent.sharedFile, hashedEvent.infoHash)
}
void onFileDownloadedEvent(FileDownloadedEvent downloadedEvent) {
if (core.getMuOptions().getShareDownloadedFiles()) {
persistFile(downloadedEvent.downloadedFile, downloadedEvent.infoHash)
}
}
/**
* Get rid of the json and hashlists of unshared files
* @param unsharedEvent
*/
void onFileUnsharedEvent(FileUnsharedEvent unsharedEvent) {
def jsonPath = getJsonPath(unsharedEvent.unsharedFile)
def jsonFile = jsonPath.toFile()
if(jsonFile.isFile()){
jsonFile.delete()
}
def hashListPath = getHashListPath(unsharedEvent.unsharedFile)
def hashListFile = hashListPath.toFile()
if (hashListFile.isFile())
hashListFile.delete()
}
void onFileLoadedEvent(FileLoadedEvent loadedEvent) {
if(loadedEvent.source == "PersisterService"){
log.info("Migrating persisted file from PersisterService: "
+ loadedEvent.loadedFile.file.absolutePath.toString())
persistFile(loadedEvent.loadedFile, loadedEvent.infoHash)
}
}
void onUICommentEvent(UICommentEvent e) {
persistFile(e.sharedFile,null)
}
void load() {
log.fine("Loading...")
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
if (location.exists() && location.isDirectory()) {
try {
_load()
}
catch (IllegalArgumentException e) {
log.log(Level.WARNING, "couldn't load files", e)
}
} else {
location.mkdirs()
listener.publish(new AllFilesLoadedEvent())
}
loaded = true
}
/**
* Loads every JSON into memory
*/
private void _load() {
int loaded = 0
def slurper = new JsonSlurper()
Files.walk(location.toPath())
.filter({
it.getFileName().toString().endsWith(".json")
})
.forEach({
def parsed = slurper.parse it.toFile()
def event = fromJsonLite parsed
if (event == null) return
log.fine("loaded file $event.loadedFile.file")
listener.publish event
loaded++
if (loaded % 10 == 0)
Thread.sleep(20)
})
listener.publish(new AllFilesLoadedEvent())
}
private void persistFile(SharedFile sf, InfoHash ih) {
persisterExecutor.submit({
def jsonPath = getJsonPath(sf)
def startTime = System.currentTimeMillis()
jsonPath.parent.toFile().mkdirs()
jsonPath.toFile().withPrintWriter { writer ->
def json = toJson sf
json = JsonOutput.toJson(json)
writer.println json
}
if (ih != null) {
def hashListPath = getHashListPath(sf)
hashListPath.toFile().bytes = ih.hashList
}
log.fine("Time(ms) to write json+hashList: " + (System.currentTimeMillis() - startTime))
} as Runnable)
}
private Path getJsonPath(SharedFile sf){
def pathHash = sf.getB64PathHash()
return Paths.get(
location.getAbsolutePath(),
pathHash.substring(0, CUT_LENGTH),
pathHash.substring(CUT_LENGTH) + ".json"
)
}
private Path getHashListPath(SharedFile sf) {
def pathHash = sf.getB64PathHash()
return Paths.get(
location.getAbsolutePath(),
pathHash.substring(0, CUT_LENGTH),
pathHash.substring(CUT_LENGTH) + ".hashlist"
)
}
InfoHash loadInfoHash(SharedFile sf) {
def path = getHashListPath(sf)
InfoHash.fromHashList(path.toFile().bytes)
}
}

View File

@ -1,40 +1,24 @@
package com.muwire.core.files
import java.nio.file.CopyOption
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.logging.Level
import java.util.stream.Collectors
import com.muwire.core.DownloadedFile
import com.muwire.core.EventBus
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.Service
import com.muwire.core.SharedFile
import com.muwire.core.UILoadedEvent
import com.muwire.core.util.DataUtil
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
class PersisterService extends Service {
class PersisterService extends BasePersisterService {
final File location
final EventBus listener
final int interval
final Timer timer
final FileManager fileManager
final ExecutorService persisterExecutor = Executors.newSingleThreadExecutor({ r ->
new Thread(r, "file persister")
} as ThreadFactory)
PersisterService(File location, EventBus listener, int interval, FileManager fileManager) {
this.location = location
@ -51,10 +35,6 @@ class PersisterService extends Service {
void onUILoadedEvent(UILoadedEvent e) {
timer.schedule({load()} as TimerTask, 1)
}
void onUIPersistFilesEvent(UIPersistFilesEvent e) {
persistFiles()
}
void load() {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY)
@ -69,6 +49,7 @@ class PersisterService extends Service {
def event = fromJson parsed
if (event != null) {
log.fine("loaded file $event.loadedFile.file")
event.source = "PersisterService"
listener.publish event
loaded++
if (loaded % 10 == 0)
@ -76,126 +57,18 @@ class PersisterService extends Service {
}
}
}
listener.publish(new AllFilesLoadedEvent())
} catch (IllegalArgumentException|NumberFormatException e) {
// Backup the old hashes
location.renameTo(
new File(location.absolutePath + ".bak")
)
listener.publish(new PersisterDoneEvent())
} catch (IllegalArgumentException e) {
log.log(Level.WARNING, "couldn't load files",e)
}
} else {
listener.publish(new AllFilesLoadedEvent())
listener.publish(new PersisterDoneEvent())
}
timer.schedule({persistFiles()} as TimerTask, 1000, interval)
loaded = true
}
private static FileLoadedEvent fromJson(def json) {
if (json.file == null || json.length == null || json.infoHash == null || json.hashList == null)
throw new IllegalArgumentException()
if (!(json.hashList instanceof List))
throw new IllegalArgumentException()
def file = new File(DataUtil.readi18nString(Base64.decode(json.file)))
file = file.getCanonicalFile()
if (!file.exists() || file.isDirectory())
return null
long length = Long.valueOf(json.length)
if (length != file.length())
return null
List hashList = (List) json.hashList
ByteArrayOutputStream baos = new ByteArrayOutputStream()
hashList.each {
byte [] hash = Base64.decode it.toString()
if (hash == null)
throw new IllegalArgumentException()
baos.write hash
}
byte[] hashListBytes = baos.toByteArray()
InfoHash ih = InfoHash.fromHashList(hashListBytes)
byte [] root = Base64.decode(json.infoHash.toString())
if (root == null)
throw new IllegalArgumentException()
if (!Arrays.equals(root, ih.getRoot()))
return null
int pieceSize = 0
if (json.pieceSize != null)
pieceSize = json.pieceSize
if (json.sources != null) {
List sources = (List)json.sources
Set<Destination> sourceSet = sources.stream().map({d -> new Destination(d.toString())}).collect Collectors.toSet()
DownloadedFile df = new DownloadedFile(file, ih, pieceSize, sourceSet)
df.setComment(json.comment)
return new FileLoadedEvent(loadedFile : df)
}
SharedFile sf = new SharedFile(file, ih, pieceSize)
sf.setComment(json.comment)
if (json.downloaders != null)
sf.getDownloaders().addAll(json.downloaders)
if (json.searchers != null) {
json.searchers.each {
Persona searcher = null
if (it.searcher != null)
searcher = new Persona(new ByteArrayInputStream(Base64.decode(it.searcher)))
long timestamp = it.timestamp
String query = it.query
sf.hit(searcher, timestamp, query)
}
}
return new FileLoadedEvent(loadedFile: sf)
}
private void persistFiles() {
persisterExecutor.submit( {
def sharedFiles = fileManager.getSharedFiles()
File tmp = File.createTempFile("muwire-files", "tmp")
tmp.deleteOnExit()
tmp.withPrintWriter { writer ->
sharedFiles.each { k, v ->
def json = toJson(k,v)
json = JsonOutput.toJson(json)
writer.println json
}
}
Files.copy(tmp.toPath(), location.toPath(), StandardCopyOption.REPLACE_EXISTING)
tmp.delete()
} as Runnable)
}
private def toJson(File f, SharedFile sf) {
def json = [:]
json.file = sf.getB64EncodedFileName()
json.length = sf.getCachedLength()
InfoHash ih = sf.getInfoHash()
json.infoHash = sf.getB64EncodedHashRoot()
json.pieceSize = sf.getPieceSize()
json.hashList = sf.getB64EncodedHashList()
json.comment = sf.getComment()
json.hits = sf.getHits()
json.downloaders = sf.getDownloaders()
if (!sf.searches.isEmpty()) {
Set searchers = new HashSet<>()
sf.searches.each {
def search = [:]
if (it.searcher != null)
search.searcher = it.searcher.toBase64()
search.timestamp = it.timestamp
search.query = it.query
searchers.add(search)
}
json.searchers = searchers
}
if (sf instanceof DownloadedFile) {
json.sources = sf.sources.stream().map( {d -> d.toBase64()}).collect(Collectors.toList())
}
json
}
}

View File

@ -1,6 +0,0 @@
package com.muwire.core.files
import com.muwire.core.Event
class UIPersistFilesEvent extends Event {
}

View File

@ -77,11 +77,11 @@ class ResultsSender {
if (it.getComment() != null) {
comment = DataUtil.readi18nString(Base64.decode(it.getComment()))
}
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def uiResultEvent = new UIResultEvent( sender : me,
name : it.getFile().getName(),
size : length,
infohash : it.getInfoHash(),
infohash : new InfoHash(it.getRoot()),
pieceSize : pieceSize,
uuid : uuid,
browse : settings.browseFiles,
@ -119,7 +119,7 @@ class ResultsSender {
me.write(os)
os.writeShort((short)results.length)
results.each {
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
def json = jsonOutput.toJson(obj)
os.writeShort((short)json.length())
@ -141,7 +141,7 @@ class ResultsSender {
os.write("\r\n".getBytes(StandardCharsets.US_ASCII))
DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(os))
results.each {
int certificates = certificateManager.getByInfoHash(it.getInfoHash()).size()
int certificates = certificateManager.getByInfoHash(new InfoHash(it.getRoot())).size()
def obj = sharedFileToObj(it, settings.browseFiles, certificates)
def json = jsonOutput.toJson(obj)
dos.writeShort((short)json.length())
@ -170,7 +170,7 @@ class ResultsSender {
obj.type = "Result"
obj.version = 2
obj.name = encodedName
obj.infohash = Base64.encode(sf.getInfoHash().getRoot())
obj.infohash = Base64.encode(sf.getRoot())
obj.size = sf.getCachedLength()
obj.pieceSize = sf.getPieceSize()

View File

@ -83,7 +83,7 @@ class UpdateClient {
}
void onFileDownloadedEvent(FileDownloadedEvent e) {
if (e.downloadedFile.infoHash != updateInfoHash)
if (e.infoHash != updateInfoHash)
return
updateDownloading = false
eventBus.publish(new UpdateDownloadedEvent(version : version, signer : signer, text : text))

View File

@ -12,6 +12,7 @@ import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Downloader
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.files.FileManager
import com.muwire.core.files.PersisterFolderService
import com.muwire.core.mesh.Mesh
import com.muwire.core.mesh.MeshManager
@ -22,6 +23,7 @@ import net.i2p.data.Base64
public class UploadManager {
private final EventBus eventBus
private final FileManager fileManager
private final PersisterFolderService persisterService
private final MeshManager meshManager
private final DownloadManager downloadManager
private final MuWireSettings props
@ -34,9 +36,11 @@ public class UploadManager {
public UploadManager(EventBus eventBus, FileManager fileManager,
MeshManager meshManager, DownloadManager downloadManager,
PersisterFolderService persisterService,
MuWireSettings props) {
this.eventBus = eventBus
this.fileManager = fileManager
this.persisterService = persisterService
this.meshManager = meshManager
this.downloadManager = downloadManager
this.props = props
@ -162,7 +166,7 @@ public class UploadManager {
InfoHash fullInfoHash
if (downloader == null) {
fullInfoHash = sharedFiles.iterator().next().infoHash
fullInfoHash = persisterService.loadInfoHash(sharedFiles.iterator().next())
} else {
byte [] hashList = downloader.getInfoHash().getHashList()
if (hashList != null && hashList.length > 0)

View File

@ -10,9 +10,9 @@ public class DownloadedFile extends SharedFile {
private final Set<Destination> sources;
public DownloadedFile(File file, InfoHash infoHash, int pieceSize, Set<Destination> sources)
public DownloadedFile(File file, byte[] root, int pieceSize, Set<Destination> sources)
throws IOException {
super(file, infoHash, pieceSize);
super(file, root, pieceSize);
this.sources = sources;
}

View File

@ -2,7 +2,10 @@ package com.muwire.core;
import java.io.File;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -16,44 +19,47 @@ import net.i2p.data.Base64;
public class SharedFile {
private final File file;
private final InfoHash infoHash;
private final byte[] root;
private final int pieceSize;
private final String cachedPath;
private final long cachedLength;
private String b64PathHash;
private final String b64EncodedFileName;
private final String b64EncodedHashRoot;
private final List<String> b64EncodedHashList;
private volatile String comment;
private final Set<String> downloaders = Collections.synchronizedSet(new HashSet<>());
private final Set<SearchEntry> searches = Collections.synchronizedSet(new HashSet<>());
public SharedFile(File file, InfoHash infoHash, int pieceSize) throws IOException {
public SharedFile(File file, byte[] root, int pieceSize) throws IOException {
this.file = file;
this.infoHash = infoHash;
this.root = root;
this.pieceSize = pieceSize;
this.cachedPath = file.getAbsolutePath();
this.cachedLength = file.length();
this.b64EncodedFileName = Base64.encode(DataUtil.encodei18nString(file.toString()));
this.b64EncodedHashRoot = Base64.encode(infoHash.getRoot());
List<String> b64List = new ArrayList<String>();
byte[] tmp = new byte[32];
for (int i = 0; i < infoHash.getHashList().length / 32; i++) {
System.arraycopy(infoHash.getHashList(), i * 32, tmp, 0, 32);
b64List.add(Base64.encode(tmp));
}
this.b64EncodedHashList = b64List;
}
public File getFile() {
return file;
}
public InfoHash getInfoHash() {
return infoHash;
public byte[] getPathHash() throws NoSuchAlgorithmException {
MessageDigest digester = MessageDigest.getInstance("SHA-256");
digester.update(file.getAbsolutePath().getBytes());
return digester.digest();
}
public String getB64PathHash() throws NoSuchAlgorithmException {
if(b64PathHash == null){
b64PathHash = Base64.encode(getPathHash());
}
return b64PathHash;
}
public byte[] getRoot() {
return root;
}
public int getPieceSize() {
@ -73,14 +79,6 @@ public class SharedFile {
return b64EncodedFileName;
}
public String getB64EncodedHashRoot() {
return b64EncodedHashRoot;
}
public List<String> getB64EncodedHashList() {
return b64EncodedHashList;
}
public String getCachedPath() {
return cachedPath;
}
@ -119,7 +117,7 @@ public class SharedFile {
@Override
public int hashCode() {
return file.hashCode() ^ infoHash.hashCode();
return file.hashCode() ^ Arrays.hashCode(root);
}
@Override
@ -127,7 +125,7 @@ public class SharedFile {
if (!(o instanceof SharedFile))
return false;
SharedFile other = (SharedFile)o;
return file.equals(other.file) && infoHash.equals(other.infoHash);
return file.equals(other.file) && Arrays.equals(root, other.root);
}
public static class SearchEntry {

View File

@ -1,5 +1,5 @@
group = com.muwire
version = 0.6.8
version = 0.6.10
i2pVersion = 0.9.44
groovyVersion = 2.4.15
slf4jVersion = 1.7.25

View File

@ -3,15 +3,11 @@ package com.muwire.gui
import griffon.core.GriffonApplication
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.core.mvc.MVCGroup
import griffon.core.mvc.MVCGroupConfiguration
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import groovy.json.StringEscapeUtils
import net.i2p.crypto.DSAEngine
import net.i2p.data.Base64
import net.i2p.data.Signature
import net.i2p.data.SigningPrivateKey
import java.awt.Desktop
import java.awt.Toolkit
@ -30,15 +26,11 @@ import com.muwire.core.Persona
import com.muwire.core.SharedFile
import com.muwire.core.SplitPattern
import com.muwire.core.download.Downloader
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.filecert.UICreateCertificateEvent
import com.muwire.core.files.DirectoryUnsharedEvent
import com.muwire.core.files.FileUnsharedEvent
import com.muwire.core.files.UIPersistFilesEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.SearchEvent
import com.muwire.core.trust.RemoteTrustList
@ -371,7 +363,6 @@ class MainFrameController {
sf.each {
core.eventBus.publish(new FileUnsharedEvent(unsharedFile : it))
}
core.eventBus.publish(new UIPersistFilesEvent())
}
@ControllerAction
@ -534,4 +525,4 @@ class MainFrameController {
core = e.getNewValue()
})
}
}
}

View File

@ -363,6 +363,8 @@ class MainFrameModel {
}
void onFileLoadedEvent(FileLoadedEvent e) {
if (e.source == "PersisterService")
return
runInsideUIAsync {
shared << e.loadedFile
loadedFiles = shared.size()

View File

@ -1,6 +1,7 @@
package com.muwire.gui
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.SharedFile
import griffon.core.artifact.GriffonModel
@ -21,6 +22,6 @@ class SharedFileModel {
public void mvcGroupInit(Map<String,String> args) {
searchers.addAll(sf.getSearches())
downloaders.addAll(sf.getDownloaders())
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(sf.infoHash,[]))
certificates.addAll(core.certificateManager.byInfoHash.getOrDefault(new InfoHash(sf.getRoot()),[]))
}
}

View File

@ -35,6 +35,7 @@ import javax.swing.tree.TreePath
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.MuWireSettings
import com.muwire.core.SharedFile
import com.muwire.core.download.Downloader
@ -289,7 +290,7 @@ class MainFrameView {
closureColumn(header : "Comments", preferredWidth : 50, type : Boolean, read : {it.getComment() != null})
closureColumn(header : "Certified", preferredWidth : 50, type : Boolean, read : {
Core core = application.context.get("core")
core.certificateManager.hasLocalCertificate(it.getInfoHash())
core.certificateManager.hasLocalCertificate(new InfoHash(it.getRoot()))
})
closureColumn(header : "Search Hits", preferredWidth: 50, type : Integer, read : {it.getHits()})
closureColumn(header : "Downloaders", preferredWidth: 50, type : Integer, read : {it.getDownloaders().size()})
@ -911,7 +912,7 @@ class MainFrameView {
String roots = ""
for (Iterator<SharedFile> iterator = selectedFiles.iterator(); iterator.hasNext(); ) {
SharedFile selected = iterator.next()
String root = Base64.encode(selected.infoHash.getRoot())
String root = Base64.encode(selected.getRoot())
roots += root
if (iterator.hasNext())
roots += "\n"

View File

@ -21,7 +21,6 @@ import com.muwire.core.files.FileTree;
import com.muwire.core.files.FileTreeCallback;
import com.muwire.core.files.FileUnsharedEvent;
import com.muwire.core.files.UICommentEvent;
import com.muwire.core.files.UIPersistFilesEvent;
import com.muwire.core.util.DataUtil;
import net.i2p.data.Base64;
@ -132,7 +131,6 @@ public class FileManager {
core.getEventBus().publish(event);
}
}
core.getEventBus().publish(new UIPersistFilesEvent());
Util.pause();
}

View File

@ -89,12 +89,13 @@ public class FilesServlet extends HttpServlet {
String comment = null;
if (sf.getComment() != null)
comment = DataUtil.readi18nString(Base64.decode(sf.getComment()));
InfoHash ih = new InfoHash(sf.getRoot());
FilesTableEntry entry = new FilesTableEntry(sf.getFile().getName(),
sf.getInfoHash(),
ih,
sf.getCachedPath(),
sf.getCachedLength(),
comment,
core.getCertificateManager().hasLocalCertificate(sf.getInfoHash()));
core.getCertificateManager().hasLocalCertificate(ih));
entries.add(entry);
});
@ -260,12 +261,13 @@ public class FilesServlet extends HttpServlet {
sb.append("<Name>").append(Util.escapeHTMLinXML(sf.getFile().getName())).append("</Name>");
sb.append("<Path>").append(Util.escapeHTMLinXML(sf.getCachedPath())).append("</Path>");
sb.append("<Size>").append(DataHelper.formatSize2Decimal(sf.getCachedLength())).append("B").append("</Size>");
sb.append("<InfoHash>").append(Base64.encode(sf.getInfoHash().getRoot())).append("</InfoHash>");
sb.append("<InfoHash>").append(Base64.encode(sf.getRoot())).append("</InfoHash>");
if (sf.getComment() != null) {
String comment = DataUtil.readi18nString(Base64.decode(sf.getComment()));
sb.append("<Comment>").append(Util.escapeHTMLinXML(comment)).append("</Comment>");
}
sb.append("<Certified>").append(core.getCertificateManager().hasLocalCertificate(sf.getInfoHash())).append("</Certified>");
InfoHash ih = new InfoHash(sf.getRoot());
sb.append("<Certified>").append(core.getCertificateManager().hasLocalCertificate(ih)).append("</Certified>");
// TODO: other stuff
sb.append("</File>");
}