diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java index 9a7828af7..628958a28 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/IVSCodeService.java @@ -15,7 +15,7 @@ public interface IVSCodeService { ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaultPageSize); - ResponseEntity browse(String namespaceName, String extensionName, String version, String path); + ResponseEntity browse(String namespaceName, String extensionName, String version, String targetPlatform, String path); String download(String namespace, String extension, String version, String targetPlatform); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java index dab0f45bb..4e38f026a 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; +import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.entities.Extension; import org.eclipse.openvsx.entities.ExtensionVersion; import org.eclipse.openvsx.entities.FileResource; @@ -386,15 +387,20 @@ public String download(String namespace, String extension, String version, Strin } @Override - public ResponseEntity browse(String namespaceName, String extensionName, String version, String path) { + public ResponseEntity browse(String namespaceName, String extensionName, String version, String targetPlatform, String path) { if(isBuiltInExtensionNamespace(namespaceName)) { return new ResponseEntity<>(("Built-in extension namespace '" + namespaceName + "' not allowed").getBytes(StandardCharsets.UTF_8), null, HttpStatus.BAD_REQUEST); } - var extVersions = repositories.findActiveExtensionVersionsByVersion(version, extensionName, namespaceName); - var extVersion = extVersions.stream().max(Comparator.comparing(ExtensionVersion::isUniversalTargetPlatform) - .thenComparing(ExtensionVersion::getTargetPlatform)) - .orElse(null); + ExtensionVersion extVersion; + if(StringUtils.isEmpty(targetPlatform)) { + var extVersions = repositories.findActiveExtensionVersionsByVersion(version, extensionName, namespaceName); + extVersion = extVersions.stream().max(Comparator.comparing(ExtensionVersion::isUniversalTargetPlatform) + .thenComparing(ExtensionVersion::getTargetPlatform)) + .orElse(null); + } else { + extVersion = repositories.findActiveExtensionVersionByVersion(version, targetPlatform, extensionName, namespaceName); + } if (extVersion == null) { throw new NotFoundException(); @@ -412,7 +418,7 @@ public ResponseEntity browse(String namespaceName, String extensionName, return exactMatch != null ? browseFile(exactMatch, namespaceName, extensionName, extVersion.getTargetPlatform(), version) - : browseDirectory(resources, namespaceName, extensionName, version, path); + : browseDirectory(resources, namespaceName, extensionName, targetPlatform, version, path); } private ResponseEntity browseFile( @@ -454,12 +460,16 @@ private ResponseEntity browseDirectory( List resources, String namespaceName, String extensionName, + String targetPlatform, String version, String path ) { if(!path.isEmpty() && !path.endsWith("/")) { path += "/"; } + if(StringUtils.isNotEmpty(targetPlatform)) { + version += "+" + targetPlatform; + } var urls = new HashSet(); var baseUrl = UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "vscode", "unpkg", namespaceName, extensionName, version); diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java index 08e7a8328..f72ed601d 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/UpstreamVSCodeService.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Strings; +import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.UpstreamProxyService; import org.eclipse.openvsx.UrlConfigService; import org.eclipse.openvsx.util.HttpHeadersUtil; @@ -76,7 +77,11 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul } @Override - public ResponseEntity browse(String namespaceName, String extensionName, String version, String path) { + public ResponseEntity browse(String namespaceName, String extensionName, String version, String targetPlatform, String path) { + if(StringUtils.isNotEmpty(targetPlatform)) { + version += "+" + targetPlatform; + } + var urlTemplate = urlConfigService.getUpstreamUrl() + "/vscode/unpkg/{namespace}/{extension}/{version}"; var uriVariables = new HashMap<>(Map.of( "namespace", namespaceName, diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java index ec697a347..43a02ed9a 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/VSCodeAPI.java @@ -9,6 +9,7 @@ ********************************************************************************/ package org.eclipse.openvsx.adapter; +import org.apache.commons.lang3.StringUtils; import org.eclipse.openvsx.util.NotFoundException; import org.eclipse.openvsx.util.TargetPlatform; import org.eclipse.openvsx.util.UrlUtil; @@ -129,12 +130,26 @@ public ResponseEntity browse( HttpServletRequest request, @PathVariable String namespaceName, @PathVariable String extensionName, - @PathVariable String version + @PathVariable String version, + @RequestParam(required = false) String target ) { + if(StringUtils.isEmpty(target)) { + var index = version.lastIndexOf('+'); + if(index >= 0 && index + 1 < version.length()) { + target = version.substring(index + 1); + if(TargetPlatform.isValid(target)) { + version = version.substring(0, index); + } + } + } + if(StringUtils.isNotEmpty(target) && !TargetPlatform.isValid(target)) { + target = null; + } + var path = UrlUtil.extractWildcardPath(request); for (var service : getVSCodeServices()) { try { - return service.browse(namespaceName, extensionName, version, path); + return service.browse(namespaceName, extensionName, version, target, path); } catch (NotFoundException exc) { // Try the next registry } diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java index 13c8ba973..b13d15819 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionRepository.java @@ -34,6 +34,8 @@ public interface ExtensionVersionRepository extends Repository findByVersionAndExtensionNameIgnoreCaseAndExtensionNamespaceNameIgnoreCase(String version, String extensionName, String namespace); Streamable findByPublishedWithUser(UserData user); diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index a89ac8beb..4c62c060f 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -337,6 +337,10 @@ public List findActiveExtensionVersionsByVersion(String versio return extensionVersionJooqRepo.findAllActiveByVersionAndExtensionNameAndNamespaceName(version, extensionName, namespaceName); } + public ExtensionVersion findActiveExtensionVersionByVersion(String version, String targetPlatform, String extensionName, String namespaceName) { + return extensionVersionRepo.findByVersionAndTargetPlatformAndExtensionNameIgnoreCaseAndExtensionNamespaceNameIgnoreCaseAndActiveTrue(version, targetPlatform, extensionName, namespaceName); + } + public List findActiveExtensionVersionsByExtensionName(String targetPlatform, String extensionName, String namespaceName) { return extensionVersionJooqRepo.findAllActiveByExtensionNameAndNamespaceName(targetPlatform, extensionName, namespaceName); } diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java index e95308ce9..a00ce5970 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -373,6 +373,80 @@ public void testBrowseTopDir() throws Exception { .andExpect(content().json("[\"http://localhost/vscode/unpkg/foo/bar/1.3.4/extension.vsixmanifest\",\"http://localhost/vscode/unpkg/foo/bar/1.3.4/extension/\"]")); } + @Test + public void testBrowseTopDirTargetPlatform() throws Exception { + var version = "1.3.4"; + var targetPlatform = "linux-x64"; + var extensionName = "bar"; + var namespaceName = "foo"; + var namespace = new Namespace(); + namespace.setName(namespaceName); + var extension = new Extension(); + extension.setId(0L); + extension.setName(extensionName); + extension.setNamespace(namespace); + var extVersion = new ExtensionVersion(); + extVersion.setId(1L); + extVersion.setVersion(version); + extVersion.setTargetPlatform(targetPlatform); + extVersion.setExtension(extension); + + Mockito.when(repositories.findActiveExtensionVersionByVersion(version, targetPlatform, extensionName, namespaceName)) + .thenReturn(extVersion); + + var vsixResource = mockFileResource(15, extVersion, "extension.vsixmanifest", RESOURCE, STORAGE_DB, "".getBytes(StandardCharsets.UTF_8)); + var manifestResource = mockFileResource(16, extVersion, "extension/package.json", RESOURCE, STORAGE_DB, "{\"package\":\"json\"}".getBytes(StandardCharsets.UTF_8)); + var readmeResource = mockFileResource(17, extVersion, "extension/README.md", RESOURCE, STORAGE_DB, "README".getBytes(StandardCharsets.UTF_8)); + var changelogResource = mockFileResource(18, extVersion, "extension/CHANGELOG.md", RESOURCE, STORAGE_DB, "CHANGELOG".getBytes(StandardCharsets.UTF_8)); + var licenseResource = mockFileResource(19, extVersion, "extension/LICENSE.txt", RESOURCE, STORAGE_DB, "LICENSE".getBytes(StandardCharsets.UTF_8)); + var iconResource = mockFileResource(20, extVersion, "extension/images/icon128.png", RESOURCE, STORAGE_DB, "ICON128".getBytes(StandardCharsets.UTF_8)); + + Mockito.when(repositories.findResourceFileResources(1L, "")) + .thenReturn(List.of(vsixResource, manifestResource, readmeResource, changelogResource, licenseResource, iconResource)); + + mockMvc.perform(get("/vscode/unpkg/{namespaceName}/{extensionName}/{version}?target={targetPlatform}", namespaceName, extensionName, version, targetPlatform)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[\"http://localhost/vscode/unpkg/foo/bar/1.3.4+linux-x64/extension.vsixmanifest\",\"http://localhost/vscode/unpkg/foo/bar/1.3.4+linux-x64/extension/\"]")); + } + + @Test + public void testBrowseTopDirVersionTargetPlatform() throws Exception { + var version = "1.3.4-rc-1+armhf"; + var targetPlatform = "darwin-x64"; + var extensionName = "bar"; + var namespaceName = "foo"; + var namespace = new Namespace(); + namespace.setName(namespaceName); + var extension = new Extension(); + extension.setId(0L); + extension.setName(extensionName); + extension.setNamespace(namespace); + var extVersion = new ExtensionVersion(); + extVersion.setId(1L); + extVersion.setVersion(version); + extVersion.setTargetPlatform(targetPlatform); + extVersion.setExtension(extension); + + Mockito.when(repositories.findActiveExtensionVersionByVersion(version, targetPlatform, extensionName, namespaceName)) + .thenReturn(extVersion); + + var vsixResource = mockFileResource(15, extVersion, "extension.vsixmanifest", RESOURCE, STORAGE_DB, "".getBytes(StandardCharsets.UTF_8)); + var manifestResource = mockFileResource(16, extVersion, "extension/package.json", RESOURCE, STORAGE_DB, "{\"package\":\"json\"}".getBytes(StandardCharsets.UTF_8)); + var readmeResource = mockFileResource(17, extVersion, "extension/README.md", RESOURCE, STORAGE_DB, "README".getBytes(StandardCharsets.UTF_8)); + var changelogResource = mockFileResource(18, extVersion, "extension/CHANGELOG.md", RESOURCE, STORAGE_DB, "CHANGELOG".getBytes(StandardCharsets.UTF_8)); + var licenseResource = mockFileResource(19, extVersion, "extension/LICENSE.txt", RESOURCE, STORAGE_DB, "LICENSE".getBytes(StandardCharsets.UTF_8)); + var iconResource = mockFileResource(20, extVersion, "extension/images/icon128.png", RESOURCE, STORAGE_DB, "ICON128".getBytes(StandardCharsets.UTF_8)); + + Mockito.when(repositories.findResourceFileResources(1L, "")) + .thenReturn(List.of(vsixResource, manifestResource, readmeResource, changelogResource, licenseResource, iconResource)); + + mockMvc.perform(get("/vscode/unpkg/{namespaceName}/{extensionName}/{version}+{targetPlatform}", namespaceName, extensionName, version, targetPlatform)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[\"http://localhost/vscode/unpkg/foo/bar/1.3.4-rc-1+armhf+darwin-x64/extension.vsixmanifest\",\"http://localhost/vscode/unpkg/foo/bar/1.3.4-rc-1+armhf+darwin-x64/extension/\"]")); + } + @Test public void testBrowseVsixManifest() throws Exception { var version = "1.3.4"; @@ -601,6 +675,95 @@ public void testBrowseIcon() throws Exception { .andExpect(content().bytes(content)); } + @Test + public void testBrowseIconTargetPlatform() throws Exception { + var version = "1.3.4"; + var targetPlatform = "win32-x64"; + var extensionName = "bar"; + var namespaceName = "foo"; + var namespace = new Namespace(); + namespace.setName(namespaceName); + var extension = new Extension(); + extension.setId(0L); + extension.setName(extensionName); + extension.setNamespace(namespace); + var extVersion = new ExtensionVersion(); + extVersion.setId(1L); + extVersion.setVersion(version); + extVersion.setTargetPlatform(targetPlatform); + extVersion.setExtension(extension); + Mockito.when(repositories.findActiveExtensionVersionByVersion(version, targetPlatform, extensionName, namespaceName)) + .thenReturn(extVersion); + + var content = "ICON128".getBytes(StandardCharsets.UTF_8); + var iconResource = mockFileResource(20, extVersion, "extension/images/icon128.png", RESOURCE, STORAGE_DB, content); + Mockito.when(repositories.findResourceFileResources(1L, "extension/images/icon128.png")) + .thenReturn(List.of(iconResource)); + + mockMvc.perform(get("/vscode/unpkg/{namespaceName}/{extensionName}/{version}/{path}?target={targetPlatform}", namespaceName, extensionName, version, "extension/images/icon128.png", targetPlatform)) + .andExpect(status().isOk()) + .andExpect(content().bytes(content)); + } + + @Test + public void testBrowseIconVersionTargetPlatform() throws Exception { + var version = "1.3.4-ga+armhf"; + var targetPlatform = "alpine-x64"; + var extensionName = "bar"; + var namespaceName = "foo"; + var namespace = new Namespace(); + namespace.setName(namespaceName); + var extension = new Extension(); + extension.setId(0L); + extension.setName(extensionName); + extension.setNamespace(namespace); + var extVersion = new ExtensionVersion(); + extVersion.setId(1L); + extVersion.setVersion(version); + extVersion.setTargetPlatform(targetPlatform); + extVersion.setExtension(extension); + Mockito.when(repositories.findActiveExtensionVersionByVersion(version, targetPlatform, extensionName, namespaceName)) + .thenReturn(extVersion); + + var content = "ICON128".getBytes(StandardCharsets.UTF_8); + var iconResource = mockFileResource(20, extVersion, "extension/images/icon128.png", RESOURCE, STORAGE_DB, content); + Mockito.when(repositories.findResourceFileResources(1L, "extension/images/icon128.png")) + .thenReturn(List.of(iconResource)); + + mockMvc.perform(get("/vscode/unpkg/{namespaceName}/{extensionName}/{version}+{targetPlatform}/{path}", namespaceName, extensionName, version, targetPlatform, "extension/images/icon128.png")) + .andExpect(status().isOk()) + .andExpect(content().bytes(content)); + } + + @Test + public void testBrowseIconVersionInvalidTargetPlatform() throws Exception { + var version = "1.3.4-ga+armhf"; + var extensionName = "bar"; + var namespaceName = "foo"; + var namespace = new Namespace(); + namespace.setName(namespaceName); + var extension = new Extension(); + extension.setId(0L); + extension.setName(extensionName); + extension.setNamespace(namespace); + var extVersion = new ExtensionVersion(); + extVersion.setId(1L); + extVersion.setVersion(version); + extVersion.setTargetPlatform(TargetPlatform.NAME_UNIVERSAL); + extVersion.setExtension(extension); + Mockito.when(repositories.findActiveExtensionVersionsByVersion(version, extensionName, namespaceName)) + .thenReturn(List.of(extVersion)); + + var content = "ICON128".getBytes(StandardCharsets.UTF_8); + var iconResource = mockFileResource(20, extVersion, "extension/images/icon128.png", RESOURCE, STORAGE_DB, content); + Mockito.when(repositories.findResourceFileResources(1L, "extension/images/icon128.png")) + .thenReturn(List.of(iconResource)); + + mockMvc.perform(get("/vscode/unpkg/{namespaceName}/{extensionName}/{version}/{path}", namespaceName, extensionName, version, "extension/images/icon128.png")) + .andExpect(status().isOk()) + .andExpect(content().bytes(content)); + } + @Test public void testDownload() throws Exception { mockExtensionVersion(); diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index 8a91d7b67..0f36fb55c 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -156,6 +156,7 @@ void testExecuteQueries() { () -> repositories.topNamespaceExtensionVersions(NOW, 1), () -> repositories.findFileResourcesByExtensionVersionIdAndType(LONG_LIST, STRING_LIST), () -> repositories.findActiveExtensionVersionsByVersion("version", "extensionName", "namespaceName"), + () -> repositories.findActiveExtensionVersionByVersion("version", "targetPlatform", "extensionName", "namespaceName"), () -> repositories.findResourceFileResources(1L, "prefix"), () -> repositories.findActiveExtensionVersions(LONG_LIST, "targetPlatform"), () -> repositories.findActiveExtension("name", "namespaceName"),