Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public ExtensionJson getExtension(String namespace, String extensionName, String
public ExtensionJson getExtension(String namespace, String extensionName, String targetPlatform, String version) {
var extVersion = findExtensionVersion(namespace, extensionName, targetPlatform, version);
var json = toExtensionVersionJson(extVersion, targetPlatform, true);
json.setDownloads(getDownloads(extVersion.getExtension(), targetPlatform, extVersion.getVersion()));
setDownloads(json, extVersion.getExtension(), targetPlatform, extVersion.getVersion());
return json;
}

Expand Down Expand Up @@ -263,22 +263,27 @@ public VersionReferencesJson getVersionReferences(
return json;
}

private Map<String, String> getDownloads(Extension extension, String targetPlatform, String version) {
private void setDownloads(ExtensionJson json, Extension extension, String targetPlatform, String version) {
var extVersions = repositories.findVersionsForUrls(extension, targetPlatform, version);
var fileUrls = storageUtil.getFileUrls(extVersions, UrlUtil.getBaseUrl(), DOWNLOAD);
return extVersions.stream()
.map(ev -> {
var files = fileUrls.get(ev.getId());
var download = files != null ? files.get(DOWNLOAD) : null;
if (download == null) {
logger.warn("Could not find download for: {}", NamingUtil.toLogFormat(ev));
return null;
} else {
return Map.entry(ev.getTargetPlatform(), download);
}
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
var files = storageUtil.getFiles(extVersions, DOWNLOAD);
var downloads = new HashMap<String, String>();
var sizes = new HashMap<String, Long>();
var baseUrl = UrlUtil.getBaseUrl();
for (var ev : extVersions) {
var download = files.getOrDefault(ev.getId(), List.of()).stream().findFirst().orElse(null);
if (download == null) {
logger.warn("Could not find download for: {}", NamingUtil.toLogFormat(ev));
continue;
}

downloads.put(ev.getTargetPlatform(), UrlUtil.createApiFileUrl(baseUrl, ev, download.getName()));
if (download.getSize() != null) {
sizes.put(ev.getTargetPlatform(), download.getSize());
}
}

json.setDownloads(downloads);
json.setDownloadSizes(sizes);
Comment thread
netomi marked this conversation as resolved.
Resolved
}

private ExtensionVersion findExtensionVersion(
Expand Down Expand Up @@ -1200,14 +1205,15 @@ public ExtensionJson toExtensionVersionJson(
}

json.setAllVersions(allVersionsJson);
var fileUrls = storageUtil.getFileUrls(
List.of(extVersion),
serverUrl,
withFileTypes(DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST));
json.setFiles(fileUrls.get(extVersion.getId()));
if (json.getFiles().containsKey(DOWNLOAD_SIG)) {
json.getFiles().put(PUBLIC_KEY, UrlUtil.getPublicKeyUrl(extVersion));
}
// getFiles rather than getFileUrls: the package size is read from the same rows as the URLs, so
// this path stays one query, and toFilesJson is the same mapping the two builders below already
// use - including the public key it adds alongside a signature.
var resources = storageUtil
.getFiles(
List.of(extVersion),
withFileTypes(DOWNLOAD, MANIFEST, ICON, README, LICENSE, CHANGELOG, VSIXMANIFEST))
.get(extVersion.getId());
json.setFiles(toFilesJson(extVersion, resources, UrlUtil.createApiFileBaseUrl(serverUrl, extVersion)));
if (json.getDependencies() != null) {
for (var ref : json.getDependencies()) {
ref.setUrl(createApiUrl(serverUrl, "api", ref.getNamespace(), ref.getExtension()));
Expand Down
19 changes: 19 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/json/ExtensionJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,15 @@ public static ExtensionJson error(String message) {
@Schema(description = "Map of download links by target platform")
private Map<String, String> downloads;

@Schema(
description = "Map of package sizes in bytes by target platform, for the same platforms as "
+ "'downloads'. A platform is left out where its size is not known, which is the case for "
+ "packages published before the registry started recording sizes and not yet backfilled. "
+ "This is the download size; the installed extension is larger by the package's "
+ "compression ratio."
Comment thread
netomi marked this conversation as resolved.
Resolved
)
private Map<String, Long> downloadSizes;

@Schema(description = "Map of target platforms by extension version")
private List<VersionTargetPlatformsJson> allTargetPlatformVersions;

Expand Down Expand Up @@ -598,6 +607,14 @@ public void setDownloads(Map<String, String> downloads) {
this.downloads = downloads;
}

public Map<String, Long> getDownloadSizes() {
return downloadSizes;
}

public void setDownloadSizes(Map<String, Long> downloadSizes) {
this.downloadSizes = downloadSizes;
}

public List<VersionTargetPlatformsJson> getAllTargetPlatformVersions() {
return allTargetPlatformVersions;
}
Expand Down Expand Up @@ -688,6 +705,7 @@ public boolean equals(Object o) {
&& Objects.equals(dependencies, that.dependencies)
&& Objects.equals(bundledExtensions, that.bundledExtensions)
&& Objects.equals(downloads, that.downloads)
&& Objects.equals(downloadSizes, that.downloadSizes)
&& Objects.equals(allTargetPlatformVersions, that.allTargetPlatformVersions)
&& Objects.equals(url, that.url)
&& Objects.equals(deprecated, that.deprecated)
Expand Down Expand Up @@ -739,6 +757,7 @@ public int hashCode() {
dependencies,
bundledExtensions,
downloads,
downloadSizes,
allTargetPlatformVersions,
url,
deprecated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,27 @@ public List<FileResource> findByType(Collection<ExtensionVersion> extVersions, C
}

var extVersionsById = extVersions.stream().collect(Collectors.toMap(ExtensionVersion::getId, ev -> ev));
return dsl.select(FILE_RESOURCE.ID, FILE_RESOURCE.EXTENSION_ID, FILE_RESOURCE.NAME, FILE_RESOURCE.TYPE)
return dsl
.select(
FILE_RESOURCE.ID,
FILE_RESOURCE.EXTENSION_ID,
FILE_RESOURCE.NAME,
FILE_RESOURCE.TYPE,
FILE_RESOURCE.SIZE)
Comment thread
netomi marked this conversation as resolved.
Resolved
.from(FILE_RESOURCE)
.where(FILE_RESOURCE.EXTENSION_ID.in(extVersionsById.keySet())).and(FILE_RESOURCE.TYPE.in(types))
.fetch()
.map(row -> toFileResource(row, extVersionsById));
}

public List<FileResource> findAll(Collection<Long> extensionIds, Collection<String> types) {
return dsl.select(FILE_RESOURCE.ID, FILE_RESOURCE.EXTENSION_ID, FILE_RESOURCE.NAME, FILE_RESOURCE.TYPE)
return dsl
.select(
FILE_RESOURCE.ID,
FILE_RESOURCE.EXTENSION_ID,
FILE_RESOURCE.NAME,
FILE_RESOURCE.TYPE,
FILE_RESOURCE.SIZE)
.from(FILE_RESOURCE)
.where(FILE_RESOURCE.EXTENSION_ID.in(extensionIds).and(FILE_RESOURCE.TYPE.in(types)))
.fetch()
Expand All @@ -79,6 +91,9 @@ private FileResource toFileResource(Record row, ExtensionVersion extVersion) {
fileResource.setId(row.get(FILE_RESOURCE.ID));
fileResource.setName(row.get(FILE_RESOURCE.NAME));
fileResource.setType(row.get(FILE_RESOURCE.TYPE));
// Null for a resource stored before V1_73 added the column that FileResourceSizeJobRequestHandler
// has not backfilled yet, so every reader has to treat the size as unknown rather than as zero.
fileResource.setSize(row.get(FILE_RESOURCE.SIZE));
fileResource.setExtension(extVersion);

return fileResource;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,28 @@ public long getFileSize(FileResource resource) throws IOException {
return storageService.getFileSize(resource);
}

/**
* The stored files of these versions, by version id, in one query.
* <p>
* {@link #getFileUrls} is the URL-only shorthand over this; a caller that needs more than the URL of a
* file - its size, say - takes the resources themselves rather than paying for a second query to read
* the other column.
*/
public Map<Long, List<FileResource>> getFiles(Collection<ExtensionVersion> extVersions, String... types) {
var byVersion = extVersions.stream()
.map(ev -> Map.<Long, List<FileResource>>entry(ev.getId(), new ArrayList<>(types.length)))
.collect(
Collectors.<Map.Entry<Long, List<FileResource>>, Long, List<FileResource>>toMap(
Map.Entry::getKey,
Map.Entry::getValue));

for (var resource : repositories.findFilesByType(extVersions, Arrays.asList(types))) {
byVersion.get(resource.getExtension().getId()).add(resource);
}

return byVersion;
}

/**
* Returns URLs for the given file types as a map of ExtensionVersion.id by a map of type by file URL, to be used in JSON response data.
*/
Expand All @@ -322,19 +344,17 @@ public Map<Long, Map<String, String>> getFileUrls(
String serverUrl,
String... types
) {
var type2Url = extVersions.stream()
.map(ev -> Map.<Long, Map<String, String>>entry(ev.getId(), new LinkedHashMap<>(types.length)))
.collect(
Collectors.<Map.Entry<Long, Map<String, String>>, Long, Map<String, String>>toMap(
Map.Entry::getKey,
Map.Entry::getValue));
var type2Url = HashMap.<Long, Map<String, String>>newHashMap(extVersions.size());
getFiles(extVersions, types).forEach((extVersionId, resources) -> {
var urls = new LinkedHashMap<String, String>(types.length);
for (var resource : resources) {
urls.put(
resource.getType(),
createApiFileUrl(serverUrl, resource.getExtension(), resource.getName()));
}

var resources = repositories.findFilesByType(extVersions, Arrays.asList(types));
for (var resource : resources) {
var extVersion = resource.getExtension();
type2Url.get(extVersion.getId())
.put(resource.getType(), createApiFileUrl(serverUrl, extVersion, resource.getName()));
}
type2Url.put(extVersionId, urls);
});

return type2Url;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ void shouldNotDeleteTempFileOnceOwnershipIsHandedToAsyncPublish() throws IOExcep
when(extensions.createExtensionFile(any())).thenReturn(tempFile);
when(tokens.useAccessToken(eq("tok"), any())).thenReturn(tau);
when(extensions.publishVersion(any(ExtensionProcessor.class), eq(tau))).thenReturn(extVersion);
when(storageUtilService.getFileUrls(any(), any(), any(String[].class))).thenReturn(Map.of(42L, Map.of()));
when(storageUtilService.getFiles(any(), any(String[].class))).thenReturn(Map.of(42L, List.of()));

registryService.publish(new ByteArrayInputStream(new byte[0]), "tok");

Expand Down
33 changes: 33 additions & 0 deletions server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ class RegistryAPITest {
*/
private static final Duration CHANGES_FEED_LAG = Duration.ofSeconds(30);

/** The size the mocked extension's .vsix reports, in bytes. */
private static final long DOWNLOAD_SIZE = 1_234_567L;

@MockitoSpyBean
UserService users;

Expand Down Expand Up @@ -287,6 +290,35 @@ void testExtension() throws Exception {
})));
}

// The size travels from file_resource.size to the JSON untouched, keyed by target platform like
// 'downloads', so a client can show how big each platform's download is without fetching it.
@Test
void testExtensionDownloadSizes() throws Exception {
var extVersion = mockExtension();
Mockito.when(repositories.findExtensionVersion("foo", "bar", null, VersionAlias.LATEST)).thenReturn(extVersion);
Mockito.when(repositories.findVersionsForUrls(any(), any(), any())).thenReturn(List.of(extVersion));

mockMvc.perform(get("/api/{namespace}/{extension}", "foo", "bar"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.downloadSizes.universal").value(DOWNLOAD_SIZE))
.andExpect(jsonPath("$.downloadSize").doesNotExist());
}

// Omitted rather than sent as 0, which a client could not tell from a genuinely empty file: a package
// published before the size column existed has none until the backfill reaches it.
@Test
void testExtensionDownloadSizesUnknown() throws Exception {
var extVersion = mockExtension();
Mockito.when(repositories.findExtensionVersion("foo", "bar", null, VersionAlias.LATEST)).thenReturn(extVersion);
Mockito.when(repositories.findVersionsForUrls(any(), any(), any())).thenReturn(List.of(extVersion));
repositories.findFilesByType(List.of(extVersion), List.of(DOWNLOAD)).forEach(file -> file.setSize(null));

mockMvc.perform(get("/api/{namespace}/{extension}", "foo", "bar"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.downloads.universal").exists())
.andExpect(jsonPath("$.downloadSizes.universal").doesNotExist());
}

@Test
void testExtensionWithPublicKey() throws Exception {
Mockito.when(integrityService.isEnabled()).thenReturn(true);
Expand Down Expand Up @@ -3261,6 +3293,7 @@ private ExtensionVersion mockExtension(String targetPlatform, boolean withSignat
download.setType(DOWNLOAD);
download.setStorageType(STORAGE_LOCAL);
download.setName("extension-1.0.0.vsix");
download.setSize(DOWNLOAD_SIZE);
var signature = new FileResource();
if (withSignature) {
signature.setExtension(extVersion);
Expand Down
24 changes: 6 additions & 18 deletions server/src/test/java/org/eclipse/openvsx/UserAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -551,12 +551,8 @@ void testGetOwnExtensionAsNamespaceOwner() throws Exception {
TargetPlatform.NAME_UNIVERSAL,
true,
false)))));
Mockito.when(
storageUtil.getFileUrls(
Mockito.anyCollection(),
Mockito.anyString(),
Mockito.any(String[].class)))
.thenReturn(java.util.Map.of(42L, new java.util.HashMap<>()));
Mockito.when(storageUtil.getFiles(Mockito.anyCollection(), Mockito.any(String[].class)))
.thenReturn(java.util.Map.of(42L, java.util.List.of()));
mockMvc.perform(
get("/user/extension/{namespace}/{extension}", "foobar", "baz")
.with(user("test_user")))
Expand Down Expand Up @@ -607,12 +603,8 @@ void testGetOwnExtensionAsNamespaceMember() throws Exception {
TargetPlatform.NAME_UNIVERSAL,
true,
false)))));
Mockito.when(
storageUtil.getFileUrls(
Mockito.anyCollection(),
Mockito.anyString(),
Mockito.any(String[].class)))
.thenReturn(java.util.Map.of(42L, new java.util.HashMap<>()));
Mockito.when(storageUtil.getFiles(Mockito.anyCollection(), Mockito.any(String[].class)))
.thenReturn(java.util.Map.of(42L, java.util.List.of()));
mockMvc.perform(
get("/user/extension/{namespace}/{extension}", "foobar", "baz")
.with(user("test_user")))
Expand All @@ -635,12 +627,8 @@ void testGetOwnExtensionNamespaceOwnershipConflict() throws Exception {
Mockito.when(repositories.findLatestVersion(eq("foobar"), eq("baz"), any(), eq(false), eq(false)))
.thenReturn(latest);
Mockito.when(repositories.findTargetPlatformsGroupedByVersion(extension)).thenReturn(List.of());
Mockito.when(
storageUtil.getFileUrls(
Mockito.anyCollection(),
Mockito.anyString(),
Mockito.any(String[].class)))
.thenReturn(java.util.Map.of(42L, new java.util.HashMap<>()));
Mockito.when(storageUtil.getFiles(Mockito.anyCollection(), Mockito.any(String[].class)))
.thenReturn(java.util.Map.of(42L, java.util.List.of()));

var scan = new ExtensionScan();
scan.setStatus(ScanStatus.QUARANTINED);
Expand Down
Loading
Loading