diff --git a/core/src/main/groovy/com/muwire/core/Core.groovy b/core/src/main/groovy/com/muwire/core/Core.groovy index 42236710..06085d5b 100644 --- a/core/src/main/groovy/com/muwire/core/Core.groovy +++ b/core/src/main/groovy/com/muwire/core/Core.groovy @@ -118,7 +118,7 @@ public class Core { private final ConnectionAcceptor connectionAcceptor private final ConnectionEstablisher connectionEstablisher private final HasherService hasherService - private final DownloadManager downloadManager + final DownloadManager downloadManager private final DirectoryWatcher directoryWatcher final FileManager fileManager final UploadManager uploadManager diff --git a/core/src/main/groovy/com/muwire/core/download/DownloadManager.groovy b/core/src/main/groovy/com/muwire/core/download/DownloadManager.groovy index 6638914f..7af9db86 100644 --- a/core/src/main/groovy/com/muwire/core/download/DownloadManager.groovy +++ b/core/src/main/groovy/com/muwire/core/download/DownloadManager.groovy @@ -242,4 +242,8 @@ public class DownloadManager { downloaders.values().each { it.stop() } Downloader.executorService.shutdownNow() } + + public boolean isDownloading(InfoHash infoHash) { + downloaders.containsKey(infoHash) + } } diff --git a/webui/src/main/java/com/muwire/webui/FeedManager.java b/webui/src/main/java/com/muwire/webui/FeedManager.java new file mode 100644 index 00000000..4807f78a --- /dev/null +++ b/webui/src/main/java/com/muwire/webui/FeedManager.java @@ -0,0 +1,103 @@ +package com.muwire.webui; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.muwire.core.Core; +import com.muwire.core.Persona; +import com.muwire.core.filefeeds.Feed; +import com.muwire.core.filefeeds.FeedFetchEvent; +import com.muwire.core.filefeeds.FeedItem; +import com.muwire.core.filefeeds.FeedItemFetchedEvent; +import com.muwire.core.filefeeds.FeedLoadedEvent; +import com.muwire.core.filefeeds.UIDownloadFeedItemEvent; +import com.muwire.core.filefeeds.UIFeedConfigurationEvent; +import com.muwire.core.filefeeds.UIFeedDeletedEvent; + +public class FeedManager { + + private final Core core; + private final Map remoteFeeds = new ConcurrentHashMap<>(); + + public FeedManager(Core core) { + this.core = core; + } + + public Map getRemoteFeeds() { + return remoteFeeds; + } + + public void onFeedLoadedEvent(FeedLoadedEvent e) { + remoteFeeds.put(e.getFeed().getPublisher(), new RemoteFeed(e.getFeed())); + } + + public void onUIFeedConfigurationEvent(UIFeedConfigurationEvent e) { + if (!e.isNewFeed()) + return; + remoteFeeds.put(e.getFeed().getPublisher(), new RemoteFeed(e.getFeed())); + } + + public void onFeedFetchEvent(FeedFetchEvent e) { + RemoteFeed feed = remoteFeeds.get(e.getHost()); + if (feed == null) + return; // hmm + feed.getFeed().setStatus(e.getStatus()); + feed.revision++; + } + + public void onFeedItemFetchedEvent(FeedItemFetchedEvent e) { + FeedItem item = e.getItem(); + RemoteFeed feed = remoteFeeds.get(item.getPublisher()); + if (feed == null) + return; // hmm + + if (feed.getFeed().isAutoDownload() && + !core.getFileManager().isShared(item.getInfoHash()) && + !core.getDownloadManager().isDownloading(item.getInfoHash())) { + File target = new File(core.getMuOptions().getDownloadLocation(), item.getName()); + UIDownloadFeedItemEvent event = new UIDownloadFeedItemEvent(); + event.setItem(item); + event.setTarget(target); + event.setSequential(feed.getFeed().isSequential()); + core.getEventBus().publish(event); + } + } + + void subscribe(Persona publisher) { + Feed feed = new Feed(publisher); + feed.setAutoDownload(core.getMuOptions().getDefaultFeedAutoDownload()); + feed.setItemsToKeep(core.getMuOptions().getDefaultFeedItemsToKeep()); + feed.setUpdateInterval(core.getMuOptions().getDefaultFeedUpdateInterval()); + feed.setSequential(core.getMuOptions().getDefaultFeedSequential()); + UIFeedConfigurationEvent event = new UIFeedConfigurationEvent(); + event.setFeed(feed); + event.setNewFeed(true); + core.getEventBus().publish(event); + } + + void unsubscribe(Persona publisher) { + remoteFeeds.remove(publisher); + UIFeedDeletedEvent event = new UIFeedDeletedEvent(); + event.setHost(publisher); + core.getEventBus().publish(event); + } + + static class RemoteFeed { + private final Feed feed; + private volatile long revision; + + RemoteFeed(Feed feed) { + this.feed = feed; + } + + public Feed getFeed() { + return feed; + } + + public long getRevision() { + return revision; + } + } + +} diff --git a/webui/src/main/java/com/muwire/webui/FeedServlet.java b/webui/src/main/java/com/muwire/webui/FeedServlet.java new file mode 100644 index 00000000..92a750d4 --- /dev/null +++ b/webui/src/main/java/com/muwire/webui/FeedServlet.java @@ -0,0 +1,237 @@ +package com.muwire.webui; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.text.Collator; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.muwire.core.Core; +import com.muwire.core.Persona; +import com.muwire.core.filefeeds.Feed; +import com.muwire.core.filefeeds.FeedItem; +import com.muwire.core.util.DataUtil; +import com.muwire.webui.FeedManager.RemoteFeed; + +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; + +public class FeedServlet extends HttpServlet { + + private FeedManager feedManager; + private Core core; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String section = req.getParameter("section"); + if (section == null) { + resp.sendError(403, "Bad section param"); + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append(""); + + if (section.equals("status")) { + List feeds = feedManager.getRemoteFeeds().values().stream(). + map(rf -> new WrappedFeed(rf, core.getFeedManager().getFeedItems(rf.getFeed().getPublisher()).size())). + collect(Collectors.toList()); + FEED_COMPARATORS.sort(feeds, req); + sb.append(""); + feeds.forEach(f -> f.toXML(sb)); + sb.append(""); + } else if (section.equals("items")) { + String publisherB64 = req.getParameter("publisher"); + if (publisherB64 == null) { + resp.sendError(403, "Bad param"); + return; + } + + Persona publisher; + try { + publisher = new Persona(new ByteArrayInputStream(Base64.decode(publisherB64))); + } catch (Exception bad) { + resp.sendError(403, "Bad param"); + return; + } + + RemoteFeed feed = feedManager.getRemoteFeeds().get(publisher); + if (feed == null) + return; // hmm + + List items = core.getFeedManager().getFeedItems(publisher).stream(). + map(item -> { + ResultStatus resultStatus = ResultStatus.AVAILABLE; + if (core.getFileManager().isShared(item.getInfoHash())) + resultStatus = ResultStatus.SHARED; + else if (core.getDownloadManager().isDownloading(item.getInfoHash())) + resultStatus = ResultStatus.DOWNLOADING; + return new WrappedFeedItem(item, resultStatus); + }).collect(Collectors.toList()); + + ITEM_COMPARATORS.sort(items, req); + + sb.append(""); + items.forEach(i -> i.toXML(sb)); + sb.append(""); + } else { + resp.sendError(403, "Bad section param"); + return; + } + + resp.setContentType("text/xml"); + resp.setCharacterEncoding("UTF-8"); + resp.setDateHeader("Expires", 0); + resp.setHeader("Pragma", "no-cache"); + resp.setHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate"); + byte[] out = sb.toString().getBytes("UTF-8"); + resp.setContentLength(out.length); + resp.getOutputStream().write(out); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String action = req.getParameter("action"); + if (action == null) { + resp.sendError(403,"Bad param"); + return; + } + if (action.equals("subscribe")) { + String personaB64 = req.getParameter("host"); + if (personaB64 == null) { + resp.sendError(403,"Bad param"); + return; + } + Persona host; + try { + host = new Persona(new ByteArrayInputStream(Base64.decode(personaB64))); + } catch (Exception bad) { + resp.sendError(403,"Bad param"); + return; + } + feedManager.subscribe(host); + Util.pause(); + } else if (action.equals("unsubscribe")) { + String personaB64 = req.getParameter("host"); + if (personaB64 == null) { + resp.sendError(403,"Bad param"); + return; + } + Persona host; + try { + host = new Persona(new ByteArrayInputStream(Base64.decode(personaB64))); + } catch (Exception bad) { + resp.sendError(403,"Bad param"); + return; + } + feedManager.unsubscribe(host); + Util.pause(); + } + } + + @Override + public void init(ServletConfig config) throws ServletException { + feedManager = (FeedManager) config.getServletContext().getAttribute("feedManager"); + core = (Core) config.getServletContext().getAttribute("core"); + } + + private static final Comparator FEED_BY_PUBLISHER = (l, r) -> { + return Collator.getInstance().compare(l.feed.getPublisher().getHumanReadableName(), r.feed.getPublisher().getHumanReadableName()); + }; + + private static final Comparator FEED_BY_FILES = (l, r) -> { + return Integer.compare(l.files, r.files); + }; + + private static final Comparator FEED_BY_LAST_UPDATED = (l, r) -> { + return Long.compare(l.feed.getLastUpdated(), r.feed.getLastUpdated()); + }; + + private static final Comparator FEED_BY_STATUS = (l, r) -> { + return Collator.getInstance().compare(l.feed.getStatus().toString(), r.feed.getStatus().toString()); + }; + + private static final ColumnComparators FEED_COMPARATORS = new ColumnComparators<>(); + static { + FEED_COMPARATORS.add("publisher", FEED_BY_PUBLISHER); + FEED_COMPARATORS.add("files", FEED_BY_FILES); + FEED_COMPARATORS.add("status", FEED_BY_STATUS); + FEED_COMPARATORS.add("lastUpdated", FEED_BY_LAST_UPDATED); + } + + private static final Comparator ITEM_BY_NAME = (l, r) -> { + return Collator.getInstance().compare(l.feedItem.getName(), r.feedItem.getName()); + }; + + private static final Comparator ITEM_BY_SIZE = (l, r) -> { + return Long.compare(l.feedItem.getSize(), r.feedItem.getSize()); + }; + + private static final Comparator ITEM_BY_STATUS = (l, r) -> { + return Collator.getInstance().compare(l.resultStatus.toString(), r.resultStatus.toString()); + }; + + private static final Comparator ITEM_BY_TIMESTAMP = (l, r) -> { + return Long.compare(l.feedItem.getTimestamp(), r.feedItem.getTimestamp()); + }; + + private static final ColumnComparators ITEM_COMPARATORS = new ColumnComparators<>(); + static { + ITEM_COMPARATORS.add("name", ITEM_BY_NAME); + ITEM_COMPARATORS.add("size", ITEM_BY_SIZE); + ITEM_COMPARATORS.add("status", ITEM_BY_STATUS); + ITEM_COMPARATORS.add("timestamp", ITEM_BY_TIMESTAMP); + } + + private static class WrappedFeedItem { + private final FeedItem feedItem; + private final ResultStatus resultStatus; + WrappedFeedItem(FeedItem feedItem, ResultStatus resultStatus) { + this.feedItem = feedItem; + this.resultStatus = resultStatus; + } + + void toXML(StringBuilder sb) { + sb.append(""); + sb.append("").append(Util.escapeHTMLinXML(feedItem.getName())).append(""); + sb.append("").append(resultStatus).append(""); + sb.append("").append(DataHelper.formatSize2Decimal(feedItem.getSize(), false)).append(""); + sb.append("").append(DataHelper.formatTime(feedItem.getTimestamp())).append(""); + sb.append("").append(Base64.encode(feedItem.getInfoHash().getRoot())).append(""); + sb.append("").append(feedItem.getCertificates()).append(""); + if (feedItem.getComment() != null) + sb.append("").append(Util.escapeHTMLinXML(DataUtil.readi18nString(Base64.decode(feedItem.getComment())))).append(""); + sb.append(""); + } + } + + private static class WrappedFeed { + private final Feed feed; + private final long revision; + private final int files; + WrappedFeed(RemoteFeed rf, int files) { + this.feed = rf.getFeed(); + this.revision = rf.getRevision(); + this.files = files; + } + + void toXML(StringBuilder sb) { + sb.append(""); + sb.append("").append(Util.escapeHTMLinXML(feed.getPublisher().getHumanReadableName())).append(""); + sb.append("").append(feed.getPublisher().toBase64()).append(""); + sb.append("").append(files).append(""); + sb.append("").append(revision).append(""); + sb.append("").append(feed.getStatus().toString()).append(""); + sb.append("").append(feed.getStatus().isActive()).append(""); + sb.append("").append(DataHelper.formatTime(feed.getLastUpdated())).append(""); + sb.append(""); + } + } +} diff --git a/webui/src/main/java/com/muwire/webui/MuWireClient.java b/webui/src/main/java/com/muwire/webui/MuWireClient.java index 56fcee89..39d4342a 100644 --- a/webui/src/main/java/com/muwire/webui/MuWireClient.java +++ b/webui/src/main/java/com/muwire/webui/MuWireClient.java @@ -24,6 +24,10 @@ import com.muwire.core.connection.DisconnectionEvent; import com.muwire.core.download.DownloadStartedEvent; import com.muwire.core.filecert.CertificateFetchEvent; import com.muwire.core.filecert.CertificateFetchedEvent; +import com.muwire.core.filefeeds.FeedFetchEvent; +import com.muwire.core.filefeeds.FeedItemFetchedEvent; +import com.muwire.core.filefeeds.FeedLoadedEvent; +import com.muwire.core.filefeeds.UIFeedConfigurationEvent; import com.muwire.core.files.AllFilesLoadedEvent; import com.muwire.core.files.FileDownloadedEvent; import com.muwire.core.files.FileHashedEvent; @@ -163,6 +167,12 @@ public class MuWireClient { core.getEventBus().register(UploadEvent.class, uploadManager); core.getEventBus().register(UploadFinishedEvent.class, uploadManager); + FeedManager feedManager = new FeedManager(core); + core.getEventBus().register(FeedLoadedEvent.class, feedManager); + core.getEventBus().register(UIFeedConfigurationEvent.class, feedManager); + core.getEventBus().register(FeedFetchEvent.class, feedManager); + core.getEventBus().register(FeedItemFetchedEvent.class, feedManager); + servletContext.setAttribute("searchManager", searchManager); servletContext.setAttribute("downloadManager", downloadManager); servletContext.setAttribute("connectionCounter", connectionCounter); @@ -171,6 +181,7 @@ public class MuWireClient { servletContext.setAttribute("trustManager", trustManager); servletContext.setAttribute("certificateManager", certificateManager); servletContext.setAttribute("uploadManager", uploadManager); + servletContext.setAttribute("feedManager", feedManager); } public String getHome() { diff --git a/webui/src/main/java/com/muwire/webui/SearchServlet.java b/webui/src/main/java/com/muwire/webui/SearchServlet.java index 8275776b..16fc00ef 100644 --- a/webui/src/main/java/com/muwire/webui/SearchServlet.java +++ b/webui/src/main/java/com/muwire/webui/SearchServlet.java @@ -103,11 +103,14 @@ public class SearchServlet extends HttpServlet { List senders = new ArrayList<>(); results.getBySender().forEach( (persona, resultsFromSender) -> { + UIResultEvent first = resultsFromSender.iterator().next(); Sender sender = new Sender(persona, core.getTrustService().getLevel(persona.getDestination()), - resultsFromSender.iterator().next().getBrowse(), + first.getBrowse(), browseManager.isBrowsing(persona), - resultsFromSender.size()); + resultsFromSender.size(), + first.getFeed(), + core.getFeedManager().getFeed(persona) != null); senders.add(sender); }); @@ -234,7 +237,9 @@ public class SearchServlet extends HttpServlet { browseManager.isBrowsing(event.getSender()), event.getComment(), event.getCertificates(), - core.getTrustService().getLevel(event.getSender().getDestination())); + core.getTrustService().getLevel(event.getSender().getDestination()), + event.getFeed(), + core.getFeedManager().getFeed(event.getSender()) != null); sendersForResult.add(senderForResult); }); @@ -284,13 +289,18 @@ public class SearchServlet extends HttpServlet { private final boolean browse; private final boolean browsing; private final int results; + private final boolean feed; + private final boolean subscribed; - Sender(Persona persona, TrustLevel trustLevel, boolean browse, boolean browsing, int results) { + Sender(Persona persona, TrustLevel trustLevel, boolean browse, boolean browsing, int results, + boolean feed, boolean subscribed) { this.persona = persona; this.trustLevel = trustLevel; this.browse = browse; this.browsing = browsing; this.results = results; + this.feed = feed; + this.subscribed = subscribed; } void toXML(StringBuilder sb) { @@ -301,6 +311,8 @@ public class SearchServlet extends HttpServlet { sb.append("").append(browse).append(""); sb.append("").append(browsing).append(""); sb.append("").append(results).append(""); + sb.append("").append(feed).append(""); + sb.append("").append(subscribed).append(""); sb.append(""); } } @@ -369,14 +381,19 @@ public class SearchServlet extends HttpServlet { private final String comment; private final int certificates; private final TrustLevel trustLevel; + private final boolean feed; + private final boolean subscribed; - SenderForResult(Persona sender, boolean browse, boolean browsing, String comment, int certificates, TrustLevel trustLevel) { + SenderForResult(Persona sender, boolean browse, boolean browsing, String comment, int certificates, TrustLevel trustLevel, + boolean feed, boolean subscribed) { this.sender = sender; this.browse = browse; this.trustLevel = trustLevel; this.browsing = browsing; this.comment = comment; this.certificates = certificates; + this.feed = feed; + this.subscribed = subscribed; } void toXML(StringBuilder sb) { @@ -389,6 +406,8 @@ public class SearchServlet extends HttpServlet { if (comment != null) sb.append("").append(Util.escapeHTMLinXML(comment)).append(""); sb.append("").append(certificates).append(""); + sb.append("").append(feed).append(""); + sb.append("").append(subscribed).append(""); sb.append(""); }