diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java index 5916dbf8e5..4db129da3e 100644 --- a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java +++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommand.java @@ -15,6 +15,7 @@ */ package org.onosproject.cli.app; +import com.google.common.io.ByteStreams; import org.apache.karaf.shell.commands.Argument; import org.apache.karaf.shell.commands.Command; import org.onosproject.app.ApplicationAdminService; @@ -38,9 +39,10 @@ public class ApplicationCommand extends AbstractShellCommand { static final String UNINSTALL = "uninstall"; static final String ACTIVATE = "activate"; static final String DEACTIVATE = "deactivate"; + static final String DOWNLOAD = "download"; @Argument(index = 0, name = "command", - description = "Command name (install|activate|deactivate|uninstall)", + description = "Command name (install|activate|deactivate|uninstall|download)", required = true, multiValued = false) String command = null; @@ -58,14 +60,20 @@ public class ApplicationCommand extends AbstractShellCommand { } } - } else { + } else if (command.equals(DOWNLOAD)) { for (String name : names) { - if (!manageApp(service, name)) { + if (!downloadApp(service, name)) { return; } } + } else { + for (String name : names) { + if (!manageApp(service, name)) { + return; + } + } + } } - } // Installs the application from input of the specified URL private boolean installApp(ApplicationAdminService service, String url) { @@ -82,6 +90,18 @@ public class ApplicationCommand extends AbstractShellCommand { return true; } + // Downloads the application bits to the standard output. + private boolean downloadApp(ApplicationAdminService service, String name) { + try { + ByteStreams.copy(service.getApplicationArchive(service.getId(name)), + System.out); + } catch (IOException e) { + error("Unable to download bits for application %s", name); + return false; + } + return true; + } + // Manages the specified application. private boolean manageApp(ApplicationAdminService service, String name) { ApplicationId appId = service.getId(name); diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java index eb53ec68b2..8164b0e57d 100644 --- a/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java +++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationCommandCompleter.java @@ -28,7 +28,7 @@ import static org.onosproject.cli.app.ApplicationCommand.*; public class ApplicationCommandCompleter extends AbstractChoicesCompleter { @Override public List choices() { - return ImmutableList.of(INSTALL, UNINSTALL, ACTIVATE, DEACTIVATE); + return ImmutableList.of(INSTALL, UNINSTALL, ACTIVATE, DEACTIVATE, DOWNLOAD); } } diff --git a/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java b/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java index 0efb1dd8ab..1848bf3dc4 100644 --- a/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java +++ b/cli/src/main/java/org/onosproject/cli/app/ApplicationNameCompleter.java @@ -63,7 +63,7 @@ public class ApplicationNameCompleter extends AbstractCompleter { // if (previousApps.contains(app.id().name())) { // continue; // } - if ("uninstall".equals(cmd) || + if ("uninstall".equals(cmd) || "download".equals(cmd) || ("activate".equals(cmd) && state == INSTALLED) || ("deactivate".equals(cmd) && state == ACTIVE)) { strings.add(app.id().name()); diff --git a/core/api/src/main/java/org/onosproject/app/ApplicationService.java b/core/api/src/main/java/org/onosproject/app/ApplicationService.java index 683f90a619..51d3eb41bf 100644 --- a/core/api/src/main/java/org/onosproject/app/ApplicationService.java +++ b/core/api/src/main/java/org/onosproject/app/ApplicationService.java @@ -20,6 +20,7 @@ import org.onosproject.core.ApplicationId; import org.onosproject.event.ListenerService; import org.onosproject.security.Permission; +import java.io.InputStream; import java.util.Set; /** @@ -74,4 +75,14 @@ public interface ApplicationService * @param hook pre-deactivation hook */ void registerDeactivateHook(ApplicationId appId, Runnable hook); + + /** + * Returns stream that contains the application OAR/JAR file contents. + * + * @param appId application identifier + * @return input stream containing the app OAR/JAR file + */ + default InputStream getApplicationArchive(ApplicationId appId) { + return null; + } } diff --git a/core/api/src/main/java/org/onosproject/app/ApplicationStore.java b/core/api/src/main/java/org/onosproject/app/ApplicationStore.java index 3544956661..bd5a66dc99 100644 --- a/core/api/src/main/java/org/onosproject/app/ApplicationStore.java +++ b/core/api/src/main/java/org/onosproject/app/ApplicationStore.java @@ -105,4 +105,14 @@ public interface ApplicationStore extends Store permissions); + /** + * Returns stream that contains the application OAR/JAR file contents. + * + * @param appId application identifier + * @return input stream containing the app OAR/JAR file + */ + default InputStream getApplicationArchive(ApplicationId appId) { + return null; + } + } diff --git a/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java b/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java index ec386076ab..92709cbd10 100644 --- a/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java +++ b/core/common/src/main/java/org/onosproject/common/app/ApplicationArchive.java @@ -199,13 +199,11 @@ public class ApplicationArchive checkState(!appFile(desc.name(), APP_XML).exists(), "Application %s already installed", desc.name()); - boolean isSelfContainedJar = false; - if (plainXml) { expandPlainApplication(cache, desc); } else { bis.reset(); - isSelfContainedJar = expandZippedApplication(bis, desc); + boolean isSelfContainedJar = expandZippedApplication(bis, desc); if (isSelfContainedJar) { bis.reset(); @@ -254,7 +252,7 @@ public class ApplicationArchive /** * Returns application archive stream for the specified application. This - * will be either the application ZIP file or the application XML file. + * will be either the application OAR file, JAR file or the plain XML file. * * @param appName application name * @return application archive stream diff --git a/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java b/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java index 3c3d60bd9f..d8b09b0c32 100644 --- a/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java +++ b/core/net/src/main/java/org/onosproject/app/impl/ApplicationManager.java @@ -179,6 +179,12 @@ public class ApplicationManager store.setPermissions(appId, permissions); } + @Override + public InputStream getApplicationArchive(ApplicationId appId) { + checkNotNull(appId, APP_ID_NULL); + return store.getApplicationArchive(appId); + } + private void updateStoreAndWaitForNotificationHandling(ApplicationId appId, Consumer storeUpdateTask) { CountDownLatch latch = new CountDownLatch(1); diff --git a/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java b/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java index 21450c0838..c64942f8b8 100644 --- a/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java +++ b/core/store/dist/src/main/java/org/onosproject/store/app/DistributedApplicationStore.java @@ -437,6 +437,11 @@ public class DistributedApplicationStore extends ApplicationArchive } } + @Override + public InputStream getApplicationArchive(ApplicationId appId) { + return getApplicationInputStream(appId.name()); + } + private class AppActivator implements Consumer { @Override public void accept(Application app) { diff --git a/tools/test/scenarios/yang-live-compile.xml b/tools/test/scenarios/yang-live-compile.xml index 7ddad20959..c87c4fcf4c 100644 --- a/tools/test/scenarios/yang-live-compile.xml +++ b/tools/test/scenarios/yang-live-compile.xml @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + diff --git a/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java index 3a4eb5cc5b..4a71ffd064 100644 --- a/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java +++ b/web/api/src/main/java/org/onosproject/rest/resources/ApplicationsWebResource.java @@ -207,6 +207,23 @@ public class ApplicationsWebResource extends AbstractWebResource { return response(appId); } + /** + * Get application OAR/JAR file. + * Returns the OAR/JAR file used to install the specified application. + * + * @param name application name + * @return 200 OK; 404; 401 + */ + @GET + @Produces(MediaType.APPLICATION_OCTET_STREAM) + @Path("{name}/bits") + public Response getAppBits(@PathParam("name") String name) { + ApplicationAdminService service = get(ApplicationAdminService.class); + ApplicationId appId = nullIsNotFound(service.getId(name), APP_ID_NOT_FOUND); + InputStream bits = service.getApplicationArchive(appId); + return ok(bits).build(); + } + /** * Gets applicationId entry by either id or name. * diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java index d245effff1..16f288a281 100644 --- a/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java +++ b/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java @@ -58,6 +58,26 @@ public class ApplicationResource extends BaseResource { return Response.ok().build(); } + /** + * Get application OAR/JAR file. + * Returns the OAR/JAR file used to install the specified application. + * + * @param name application name + * @return 200 OK; 404; 401 + */ + @GET + @Produces(MediaType.APPLICATION_OCTET_STREAM) + @Path("{name}/download") + public Response download(@PathParam("name") String name) { + ApplicationAdminService service = get(ApplicationAdminService.class); + ApplicationId appId = service.getId(name); + InputStream bits = service.getApplicationArchive(appId); + String fileName = appId.name() + ".oar"; + return Response.ok(bits) + .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"") + .build(); + } + @Path("{name}/icon") @GET @Produces("image/png") diff --git a/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties b/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties index 5ce011af6b..b362eddd9f 100644 --- a/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties +++ b/web/gui/src/main/resources/org/onosproject/ui/lion/core/view/App.properties @@ -26,6 +26,7 @@ tt_ctl_upload=Upload an application (.oar file) tt_ctl_activate=Activate selected application tt_ctl_deactivate=Deactivate selected application tt_ctl_uninstall=Uninstall selected application +tt_ctl_download=Download selected application (.oar file) # Quick-Help panel qh_hint_esc=Deselect application diff --git a/web/gui/src/main/webapp/app/view/app/app.css b/web/gui/src/main/webapp/app/view/app/app.css index 9eb8ba8f3c..4d61f8147d 100644 --- a/web/gui/src/main/webapp/app/view/app/app.css +++ b/web/gui/src/main/webapp/app/view/app/app.css @@ -23,7 +23,7 @@ } #ov-app div.ctrl-btns { - width: 250px; + width: 290px; } /* -- Drag-n-Drop oar file upload -- */ diff --git a/web/gui/src/main/webapp/app/view/app/app.html b/web/gui/src/main/webapp/app/view/app/app.html index 6eaa126ca1..e371248f5d 100644 --- a/web/gui/src/main/webapp/app/view/app/app.html +++ b/web/gui/src/main/webapp/app/view/app/app.html @@ -38,6 +38,12 @@ tooltip tt-msg="uninstallTip" ng-class="{active: ctrlBtnState.selection}"> + +
+
diff --git a/web/gui/src/main/webapp/app/view/app/app.js b/web/gui/src/main/webapp/app/view/app/app.js index a01dd2eb37..5a2272e9a3 100644 --- a/web/gui/src/main/webapp/app/view/app/app.js +++ b/web/gui/src/main/webapp/app/view/app/app.js @@ -45,8 +45,9 @@ detailsResp = 'appDetailsResponse', fileUploadUrl = 'applications/upload', activateOption = '?activate=true', - iconUrlPrefix = 'rs/applications/', + appUrlPrefix = 'rs/applications/', iconUrlSuffix = '/icon', + downloadSuffix = '/download', dialogId = 'app-dialog', dialogOpts = { edge: 'right', @@ -167,7 +168,7 @@ } function addIcon(elem, value) { - elem.append('img').attr('src', iconUrlPrefix + value + iconUrlSuffix); + elem.append('img').attr('src', appUrlPrefix + value + iconUrlSuffix); } function populateTop(details) { @@ -250,6 +251,7 @@ $scope.activateTip = lion('tt_ctl_activate'); $scope.deactivateTip = lion('tt_ctl_deactivate'); $scope.uninstallTip = lion('tt_ctl_uninstall'); + $scope.downloadTip = lion('tt_ctl_download'); var handlers = {}; @@ -359,6 +361,12 @@ } }; + $scope.downloadApp = function () { + if ($scope.ctrlBtnState.selection) { + window.location = appUrlPrefix + $scope.selId + downloadSuffix; + } + }; + $scope.$on('FileChanged', function () { var formData = new FormData(), url;