package controllers; import java.io.File; import java.io.IOException; import java.util.*; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.openseedbox.Config; import com.openseedbox.code.MessageException; import com.openseedbox.code.Util; import com.openseedbox.jobs.GenericJob; import com.openseedbox.jobs.GenericJobResult; import com.openseedbox.jobs.torrent.*; import com.openseedbox.models.*; import com.openseedbox.models.TorrentEvent.TorrentEventType; import com.openseedbox.plugins.OpenseedboxPlugin; import com.openseedbox.plugins.OpenseedboxPlugin.PluginSearchResult; import com.openseedbox.plugins.PluginManager; import play.Logger; import play.cache.Cache; import play.data.binding.As; import play.libs.F.Promise; import play.libs.WS; import play.libs.WS.HttpResponse; import play.mvc.Before; public class Client extends Base { @Before(unless={"newUser"}) public static void checkPlan() { User u = getCurrentUser(); //check that a plan has been purchased if (u != null && u.getPlan() == null) { newUser(); } } @Before(unless={"login","auth"}) public static void before() { User u = getCurrentUser(); if (u == null) { Auth.login(); } //check that limits have not been exceeded. if they have, pause all the torrents and notify user if (u.hasExceededLimits()) { List<UserTorrent> running = u.getRunningTorrents(); for (UserTorrent ut : running) { new StartStopTorrentJob(ut.getTorrentHash(), TorrentAction.STOP, u.getId()).now(); ut.setPaused(true); } UserTorrent.batch().update(running); setGeneralErrorMessage("You have exceeded your plan limits! All your torrents will be paused until you free up some space."); } } public static void index(String group) { if (!StringUtils.isEmpty(group)) { setCurrentGroupName(group); } group = getCurrentGroupName(); renderArgs.put("currentGroup", group); renderArgs.put("users", Util.toSelectItems(User.all().fetch(), "id", "emailAddress")); User user = getCurrentUser(); List<UserTorrent> torrents = user.getTorrentsInGroup(group); List<String> groups = user.getGroups(); String torrentList = renderTorrentList(group); List<OpenseedboxPlugin> searchPlugins = PluginManager.getSearchPlugins(); List<UserMessage> userMessages = UserMessage.retrieveForUser(user); renderTemplate("client/index.html", torrentList, groups, torrents, searchPlugins, userMessages); } public static void update(String group) { //this is intended to be invoked via ajax List<UserMessage> messages = UserMessage.retrieveForUser(getCurrentUser()); List<Object> messagesAsObjects = new ArrayList<Object>(); for (UserMessage um : messages) { messagesAsObjects.add(Util.convertToMap(new Object[] { "state", um.getState(), "heading", um.getHeading(), "message", um.getMessage() })); } result(Util.convertToMap(new Object[] { "torrent-list", renderTorrentList(group), "user-messages", messagesAsObjects })); } private static String renderTorrentList(String group) { if (StringUtils.isEmpty(group)) { group = "Ungrouped"; } List<UserTorrent> torrents = getCurrentUser().getTorrentsInGroup(group); List<TorrentEvent> torrentAddEvents = TorrentEvent.getIncompleteForUser(getCurrentUser(), TorrentEventType.ADDING); List<TorrentEvent> torrentRemoveEvents = TorrentEvent.getIncompleteForUser(getCurrentUser(), TorrentEventType.REMOVING); return renderToString("client/torrent-list.html", Util.convertToMap( new Object[] { "torrents", torrents, "torrentAddEvents", torrentAddEvents, "torrentRemoveEvents", torrentRemoveEvents })); } public static void addTorrent(@As("\n") final String[] urlOrMagnet, final File[] fileFromComputer) throws IOException { if (urlOrMagnet == null && fileFromComputer == null) { setGeneralErrorMessage("Please enter a valid URL or magent link, or choose a valid file to upload."); } else { int count = 0; User user = getCurrentUser(); if (urlOrMagnet != null) { for (String s : urlOrMagnet) { new AddTorrentJob(s, null, user.getId(), getCurrentGroupName()).now(); count++; } } if (fileFromComputer != null) { for (File f : fileFromComputer) { //copy the file to somewhere more permanent because Play! deletes it when the action completes so the Job cant use it File tempFile = File.createTempFile(UUID.randomUUID().toString(), ".torrent"); FileUtils.copyFile(f, tempFile); new AddTorrentJob(null, tempFile, user.getId(), getCurrentGroupName()).now(); count++; } } if (count > 0) { if (count > 1) { setGeneralMessage(count + " torrents have been scheduled for downloading! They will begin shortly."); } else { setGeneralMessage("Your torrent has been scheduled for downloading! It will begin shortly."); } } } index(getCurrentGroupName()); } public static void search(String query, String providerClass) { List<Map<String, Object>> ret = new ArrayList<Map<String, Object>>(); try { OpenseedboxPlugin provider = (OpenseedboxPlugin) Class.forName(providerClass).newInstance(); if (provider.isSearchPlugin()) { List<PluginSearchResult> res = provider.doSearch(query); for (PluginSearchResult psr : res) { ret.add(Util.convertToMap(new Object[] { "label", String.format("%s", psr.getTorrentName()), "url", psr.getTorrentUrl() })); } } } catch (ClassNotFoundException ex) { resultError("Unable to find class: " + providerClass); } catch (InstantiationException ex) { Logger.error(ex, "Unable to instantiate class: %s", providerClass); } catch (IllegalAccessException ex) { Logger.error(ex, "Unable to instantiate class: %s", providerClass); } renderJSON(ret); } public static void torrentInfo(String hash) { //torrent info is seeders, peers, files, tracker stats final UserTorrent fromDb = UserTorrent.getByUser(getCurrentUser(), hash); if (fromDb == null) { resultError("No such torrent for user: " + hash); } Promise<GenericJobResult> p = new GenericJob() { @Override public Object doGenericJob() { //trigger the caching of these objects, inside a job because the WS //calls could take ages fromDb.getTorrent().getPeers(); fromDb.getTorrent().getTrackers(); fromDb.getTorrent().getFiles(); return fromDb; } }.now(); GenericJobResult res = await(p); if (res.hasError()) { resultError(res.getError().getMessage()); } UserTorrent torrent = (UserTorrent) res.getResult(); renderTemplate("client/torrent-info.html", torrent); } public static void torrentDownload(String hash, String type) { //torrent download is just for files final UserTorrent fromDb = UserTorrent.getByUser(getCurrentUser(), hash); if (fromDb == null) { resultError("No such torrent for user: " + hash); } if (StringUtils.equalsIgnoreCase(type, "zip")) { Promise<GenericJobResult> p = new GenericJob() { @Override protected Object doGenericJob() throws Exception { Node n = fromDb.getTorrent().getNode(); HttpResponse res = WS.url(fromDb.getTorrent().getZipDownloadLink()).get(); JsonObject result = n.handleWebServiceResponse(res).getAsJsonObject(); JsonElement dl = result.get("download-link"); String downloadLink = (dl != null) ? dl.getAsString() : null; return Util.convertToMap(new Object[] { "percent-complete", result.get("percent-complete").getAsString(), "download-link", downloadLink }); } }.now(); GenericJobResult res = await(p); if (res.hasError()) { resultError(res.getError().getMessage()); } result(res.getResult()); } else { Promise<GenericJobResult> p = new GenericJob() { @Override public Object doGenericJob() { fromDb.getTorrent().getFiles(); return fromDb; } }.now(); GenericJobResult res = await(p); if (res.hasError()) { resultError(res.getError().getMessage()); } UserTorrent torrent = (UserTorrent) res.getResult(); renderTemplate("client/torrent-download.html", torrent); } } public static void addGroup(String group) { if (!StringUtils.isBlank(group)) { User user = getCurrentUser(); List<String> groups = user.getGroups(); if (group.length() > 12) { group = group.substring(0, 12); } if (groups.contains(group)) { setGeneralErrorMessage("Group '" + group + "' already exists!"); } else { groups.add(group); user.setGroups(groups); user.save(); Account.uncacheUser(); } } else { setGeneralErrorMessage("Please enter a group name."); } index(getCurrentGroupName()); } public static void removeGroup(String group) { if (!StringUtils.isBlank(group)) { User user = getCurrentUser(); user.removeTorrentGroup(group); UserTorrent.blankOutGroup(user, group); Account.uncacheUser(); } else { setGeneralErrorMessage("Please enter a group to remove."); } String currentGroup = getCurrentGroupName(); if (!currentGroup.equals(group)) { index(getCurrentGroupName()); } index(null); } public static void addToGroup(@As(",") List<String> hashes, String group, String new_group) { User user = getCurrentUser(); List<UserTorrent> uts = UserTorrent.getByUser(getCurrentUser(), hashes); if (!StringUtils.isEmpty(new_group)) { if (new_group.length() > 12) { new_group = new_group.substring(0, 12); } user.addTorrentGroup(new_group); } String groupName = (!StringUtils.isEmpty(new_group)) ? new_group : group; if (!getCurrentGroupName().equals(groupName)) { for (UserTorrent ut : uts) { if (groupName.equals(User.TORRENT_GROUP_UNGROUPED)) { ut.setGroupName(null); } else { ut.setGroupName(groupName); } } UserTorrent.batch().update(uts); Account.uncacheUser(); } index(groupName); } public static void removeFromGroup(String group) { UserTorrent.blankOutGroup(getCurrentUser(), group); index(group); } public static void action(String what, String hash, @As(",") List<String> hashes) { if (!StringUtils.isEmpty(hash)) { hashes = new ArrayList<String>(); } if (hashes.isEmpty()) { if (StringUtils.isEmpty(hash)) { setGeneralErrorMessage("Please specify a 'hash' or 'hashes'"); } hashes.add(hash); } if (!hashes.isEmpty() && !StringUtils.isEmpty(what)) { if (what.equals("start")) { doTorrentAction(hashes, TorrentAction.START); } else if (what.equals("stop")) { doTorrentAction(hashes, TorrentAction.STOP); } else if (what.equals("remove")) { doTorrentAction(hashes, TorrentAction.REMOVE); } } else { setGeneralErrorMessage("Please specify an 'action'"); } index(getCurrentGroupName()); } public enum TorrentAction { START, STOP, REMOVE } private static void doTorrentAction(List<String> hashes, TorrentAction action) { User user = getCurrentUser(); if (action == TorrentAction.REMOVE) { for (String h : hashes) { if (StringUtils.isEmpty(h)) { continue; } new RemoveTorrentJob(h, user.getId()).now(); } if (hashes.size() > 1) { setGeneralMessage(hashes.size() + " torrents are now scheduled for deletion."); } else { setGeneralMessage("This torrent is now scheduled for deletion."); } } else { for (String hash : hashes) { UserTorrent ut = UserTorrent.getByUser(user, hash); if (ut == null) { throw new MessageException("User has no such torrent with hash: " + hash); } ut.setPaused(action == TorrentAction.STOP); ut.setRunning(action == TorrentAction.START); ut.save(); //so client updates instantly new StartStopTorrentJob(hash, action, user.getId()).now(); } } } protected static Object successOrError(Promise<GenericJobResult> p) { if (p == null) { throw new IllegalArgumentException("You cant give me a null promise!"); } GenericJobResult res = await(p); if (res == null) { return null; } if (res.hasError()) { if (res.getError() instanceof MessageException) { setGeneralErrorMessage(res.getError().getMessage()); return null; } if (StringUtils.contains(res.getError().getMessage(), "Connection refused")) { setGeneralErrorMessage("Unable to connect to backend! The administrators have been notified."); //TODO: send error email return null; } Logger.info(res.getError(), "Error occured in job."); throw new RuntimeException(res.getError()); } return res.getResult(); } public static void newUser() { render("client/new-user.html"); } public static void switchUser(long user_id) { User u = getCurrentUser(); if (!u.isAdmin()) { resultError("You have to be admin to do this!"); } session.put("currentUserId", user_id); Cache.clear(); index(null); } public static void downloadMultiple(@As(",") List<String> hashes, String debug) { if (!Config.isZipEnabled()) { notFound("Zip has been disabled."); } //Since the torrents are spread out over multiple nodes, we cant zip up multiple torrents into a single zip at the node level //Basically, we send HEAD requests to get the Content-Length, and then point nginx's mod_zip to an internal route that makes nginx fetch the torrent zip files from the upstream server if (hashes == null || hashes.isEmpty()) { notFound("Please specify some hashes."); } String all = ""; for (Torrent t : Torrent.getByHash(hashes)) { String link = t.getZipDownloadLink(); String len = WS.url(link).head().getHeader("Content-Length"); //strip scheme and replace localhost with 127.0.0.1 so nginx resolver doesnt time out link = link.replace("http://", "").replace("https://", "").replace("localhost", "127.0.0.1"); link += "&scheme=" + t.getNode().getScheme(); //note: we specify scheme here to help nginx incase the frontend is running on http and the backend is running on https //no CRC32 because theres no way of knowing the CRC32 of the upstream zipfiles without downloading them first all += String.format("- %s %s/%s /%s\n", len, Config.getZipPath(), link, t.getName() + ".zip"); } if (!Config.isZipManifestOnly() && debug == null) { response.setHeader("X-Archive-Files", "zip"); //tell NGINX to create us a zip response.setHeader("Content-Disposition", "attachment; filename=\"" + UUID.randomUUID().toString() + ".zip" + "\""); } response.setHeader("Last-Modified", Util.getLastModifiedHeader(System.currentTimeMillis())); renderText(all); } //intended to be called via ajax public static void updateGroupOrder(List<String> newOrder) { if (newOrder != null && !newOrder.isEmpty()) { User u = getCurrentUser(); u.setGroups(newOrder); u.save(); Account.uncacheUser(); } result(Util.convertToMap(new Object[] { "success", true })); } private static void setCurrentGroupName(String group) { Cache.set(getCurrentGroupNameCacheKey(), group, "3d"); } private static String getCurrentGroupName() { String name = Cache.get(getCurrentGroupNameCacheKey(), String.class); if (StringUtils.isEmpty(name)) { return User.TORRENT_GROUP_UNGROUPED; } return name; } private static String getCurrentGroupNameCacheKey() { return session.getId() + "_currentGroupName"; } }