Skip to content

Commit 9f36399

Browse files
author
amvanbaren
committed
Web extension resources return incorrect MIME type
Use [Content_Types].xml to set FileResource contentType Add test case for default content type: application/octet-stream Add migration to set FileResource.contentType Set contentType for RESOURCE type Prepend dot ('.') if extension doesn't start with a dot.
1 parent f061c72 commit 9f36399

39 files changed

+886
-142
lines changed

server/src/main/java/org/eclipse/openvsx/ExtensionProcessor.java

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
********************************************************************************/
1010
package org.eclipse.openvsx;
1111

12+
import java.io.ByteArrayInputStream;
1213
import java.io.EOFException;
1314
import java.io.IOException;
1415
import java.nio.file.Path;
@@ -36,6 +37,11 @@
3637
import org.slf4j.Logger;
3738
import org.slf4j.LoggerFactory;
3839
import org.springframework.data.util.Pair;
40+
import org.springframework.http.MediaType;
41+
import org.xml.sax.SAXException;
42+
43+
import javax.xml.parsers.DocumentBuilderFactory;
44+
import javax.xml.parsers.ParserConfigurationException;
3945

4046
/**
4147
* Processes uploaded extension files and extracts their metadata.
@@ -282,17 +288,21 @@ private List<String> getEngines(JsonNode node) {
282288
}
283289

284290
public List<FileResource> getFileResources(ExtensionVersion extVersion) {
285-
var resources = new ArrayList<FileResource>();
291+
var contentTypes = loadContentTypes();
286292
var mappers = List.<Function<ExtensionVersion, FileResource>>of(
287293
this::getManifest, this::getReadme, this::getChangelog, this::getLicense, this::getIcon
288294
);
289295

290-
mappers.forEach(mapper -> Optional.of(extVersion).map(mapper).ifPresent(resources::add));
291-
return resources;
296+
return mappers.stream()
297+
.map(mapper -> mapper.apply(extVersion))
298+
.filter(Objects::nonNull)
299+
.map(resource -> setContentType(resource, contentTypes))
300+
.collect(Collectors.toList());
292301
}
293302

294303
public void processEachResource(ExtensionVersion extVersion, Consumer<FileResource> processor) {
295304
readInputStream();
305+
var contentTypes = loadContentTypes();
296306
zipFile.stream()
297307
.filter(zipEntry -> !zipEntry.isDirectory())
298308
.map(zipEntry -> {
@@ -311,6 +321,7 @@ public void processEachResource(ExtensionVersion extVersion, Consumer<FileResour
311321
resource.setName(zipEntry.getName());
312322
resource.setType(FileResource.RESOURCE);
313323
resource.setContent(bytes);
324+
setContentType(resource, contentTypes);
314325
return resource;
315326
})
316327
.filter(Objects::nonNull)
@@ -413,6 +424,46 @@ public FileResource getLicense(ExtensionVersion extVersion) {
413424
return license;
414425
}
415426

427+
private Map<String, String> loadContentTypes() {
428+
var bytes = ArchiveUtil.readEntry(zipFile, "[Content_Types].xml");
429+
var contentTypes = parseContentTypesXml(bytes);
430+
contentTypes.putIfAbsent(".vsix", "application/zip");
431+
return contentTypes;
432+
}
433+
434+
private Map<String, String> parseContentTypesXml(byte[] content) {
435+
var contentTypes = new HashMap<String, String>();
436+
try (var input = new ByteArrayInputStream(content)) {
437+
var document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(input);
438+
var elements = document.getDocumentElement().getElementsByTagName("Default");
439+
for(var i = 0; i < elements.getLength(); i++) {
440+
var element = elements.item(i);
441+
var attributes = element.getAttributes();
442+
var extension = attributes.getNamedItem("Extension").getTextContent();
443+
if(!extension.startsWith(".")) {
444+
extension = "." + extension;
445+
}
446+
447+
var contentType = attributes.getNamedItem("ContentType").getTextContent();
448+
contentTypes.put(extension, contentType);
449+
}
450+
} catch (IOException | ParserConfigurationException | SAXException e) {
451+
logger.error("failed to read content types", e);
452+
contentTypes.clear();
453+
}
454+
455+
return contentTypes;
456+
}
457+
458+
private FileResource setContentType(FileResource resource, Map<String, String> contentTypes) {
459+
var resourceName = Optional.ofNullable(resource.getName()).orElse("");
460+
var fileExtensionIndex = resourceName.lastIndexOf('.');
461+
var fileExtension = fileExtensionIndex != -1 ? resourceName.substring(fileExtensionIndex) : "";
462+
var contentType = contentTypes.getOrDefault(fileExtension, MediaType.APPLICATION_OCTET_STREAM_VALUE);
463+
resource.setContentType(contentType);
464+
return resource;
465+
}
466+
416467
private void detectLicense(byte[] content, ExtensionVersion extVersion) {
417468
if (Strings.isNullOrEmpty(extVersion.getLicense())) {
418469
var detection = new LicenseDetection();

server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ private ResponseEntity<byte[]> browseFile(
404404
String version
405405
) {
406406
if (resource.getStorageType().equals(FileResource.STORAGE_DB)) {
407-
var headers = storageUtil.getFileResponseHeaders(resource.getName());
407+
var headers = storageUtil.getFileResponseHeaders(resource);
408408
return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK);
409409
} else {
410410
var namespace = new Namespace();

server/src/main/java/org/eclipse/openvsx/entities/FileResource.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public class FileResource {
4545
@Basic(fetch = FetchType.LAZY)
4646
byte[] content;
4747

48+
String contentType;
49+
4850
@Column(length = 32)
4951
String storageType;
5052

@@ -88,6 +90,14 @@ public void setContent(byte[] content) {
8890
this.content = content;
8991
}
9092

93+
public String getContentType() {
94+
return contentType;
95+
}
96+
97+
public void setContentType(String contentType) {
98+
this.contentType = contentType;
99+
}
100+
91101
public String getStorageType() {
92102
return storageType;
93103
}

server/src/main/java/org/eclipse/openvsx/migration/ExtractResourcesJobRequestHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package org.eclipse.openvsx.migration;
1111

1212
import org.eclipse.openvsx.ExtensionProcessor;
13+
import org.eclipse.openvsx.entities.ExtensionVersion;
1314
import org.jobrunr.jobs.annotations.Job;
1415
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
1516
import org.jobrunr.jobs.lambdas.JobRequestHandler;
@@ -32,11 +33,11 @@ public class ExtractResourcesJobRequestHandler implements JobRequestHandler<Migr
3233
@Override
3334
@Job(name = "Extract resources from published extension version", retries = 3)
3435
public void run(MigrationJobRequest jobRequest) throws Exception {
35-
var extVersion = service.getExtension(jobRequest.getEntityId());
36+
var extVersion = migrations.find(jobRequest, ExtensionVersion.class);
3637
logger.info("Extracting resources for: {}.{}-{}@{}", extVersion.getExtension().getNamespace().getName(), extVersion.getExtension().getName(), extVersion.getVersion(), extVersion.getTargetPlatform());
3738

3839
service.deleteResources(extVersion);
39-
var entry = service.getDownload(extVersion);
40+
var entry = migrations.getDownload(extVersion);
4041
var extensionFile = migrations.getExtensionFile(entry);
4142
var download = entry.getKey();
4243
try(var extProcessor = new ExtensionProcessor(extensionFile)) {

server/src/main/java/org/eclipse/openvsx/migration/ExtractResourcesJobService.java

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,11 @@
1212
import org.eclipse.openvsx.entities.ExtensionVersion;
1313
import org.eclipse.openvsx.entities.FileResource;
1414
import org.eclipse.openvsx.repositories.RepositoryService;
15-
import org.eclipse.openvsx.storage.AzureBlobStorageService;
16-
import org.eclipse.openvsx.storage.GoogleCloudStorageService;
17-
import org.eclipse.openvsx.storage.IStorageService;
1815
import org.springframework.beans.factory.annotation.Autowired;
19-
import org.springframework.http.HttpMethod;
20-
import org.springframework.retry.annotation.Retryable;
2116
import org.springframework.stereotype.Component;
22-
import org.springframework.web.client.RestTemplate;
2317

2418
import javax.persistence.EntityManager;
2519
import javax.transaction.Transactional;
26-
import java.io.IOException;
27-
import java.nio.file.Files;
28-
import java.nio.file.Path;
2920
import java.util.AbstractMap;
3021
import java.util.Map;
3122

@@ -35,22 +26,9 @@ public class ExtractResourcesJobService {
3526
@Autowired
3627
RepositoryService repositories;
3728

38-
@Autowired
39-
RestTemplate restTemplate;
40-
4129
@Autowired
4230
EntityManager entityManager;
4331

44-
@Autowired
45-
AzureBlobStorageService azureStorage;
46-
47-
@Autowired
48-
GoogleCloudStorageService googleStorage;
49-
50-
public ExtensionVersion getExtension(long entityId) {
51-
return entityManager.find(ExtensionVersion.class, entityId);
52-
}
53-
5432
@Transactional
5533
public void deleteResources(ExtensionVersion extVersion) {
5634
repositories.deleteFileResources(extVersion, "resource");
@@ -61,13 +39,6 @@ public void deleteWebResources(ExtensionVersion extVersion) {
6139
repositories.deleteFileResources(extVersion, "web-resource");
6240
}
6341

64-
@Transactional
65-
public Map.Entry<FileResource, byte[]> getDownload(ExtensionVersion extVersion) {
66-
var download = repositories.findFileByType(extVersion, FileResource.DOWNLOAD);
67-
var content = download.getStorageType().equals(FileResource.STORAGE_DB) ? download.getContent() : null;
68-
return new AbstractMap.SimpleEntry<>(download, content);
69-
}
70-
7142
@Transactional
7243
public void persistResource(FileResource resource) {
7344
entityManager.persist(resource);

server/src/main/java/org/eclipse/openvsx/migration/MigrationRunner.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public void runMigrations(ApplicationStartedEvent event) {
4141
extractResourcesMigration();
4242
setPreReleaseMigration();
4343
renameDownloadsMigration();
44+
setContentTypeMigration();
4445
}
4546

4647
private void extractResourcesMigration() {
@@ -61,6 +62,12 @@ private void renameDownloadsMigration() {
6162
repositories.findNotMigratedRenamedDownloads().forEach(item -> enqueueJob(jobName, handler, item));
6263
}
6364

65+
private void setContentTypeMigration() {
66+
var jobName = "SetContentTypeMigration";
67+
var handler = SetContentTypeJobRequestHandler.class;
68+
repositories.findNotMigratedContentTypes().forEach(item -> enqueueJob(jobName, handler, item));
69+
}
70+
6471
private void enqueueJob(String jobName, Class<? extends JobRequestHandler<MigrationJobRequest>> handler, MigrationItem item) {
6572
var jobIdText = jobName + "::itemId=" + item.getId();
6673
var jobId = UUID.nameUUIDFromBytes(jobIdText.getBytes(StandardCharsets.UTF_8));

server/src/main/java/org/eclipse/openvsx/migration/MigrationService.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
* ****************************************************************************** */
1010
package org.eclipse.openvsx.migration;
1111

12+
import org.eclipse.openvsx.entities.ExtensionVersion;
1213
import org.eclipse.openvsx.entities.FileResource;
14+
import org.eclipse.openvsx.repositories.RepositoryService;
1315
import org.eclipse.openvsx.storage.AzureBlobStorageService;
1416
import org.eclipse.openvsx.storage.GoogleCloudStorageService;
1517
import org.eclipse.openvsx.storage.IStorageService;
@@ -19,14 +21,23 @@
1921
import org.springframework.stereotype.Component;
2022
import org.springframework.web.client.RestTemplate;
2123

24+
import javax.persistence.EntityManager;
25+
import javax.transaction.Transactional;
2226
import java.io.IOException;
2327
import java.nio.file.Files;
2428
import java.nio.file.Path;
29+
import java.util.AbstractMap;
2530
import java.util.Map;
2631

2732
@Component
2833
public class MigrationService {
2934

35+
@Autowired
36+
EntityManager entityManager;
37+
38+
@Autowired
39+
RepositoryService repositories;
40+
3041
@Autowired
3142
RestTemplate restTemplate;
3243

@@ -36,6 +47,17 @@ public class MigrationService {
3647
@Autowired
3748
GoogleCloudStorageService googleStorage;
3849

50+
public <T> T find(MigrationJobRequest jobRequest, Class<T> clazz) {
51+
return entityManager.find(clazz, jobRequest.getEntityId());
52+
}
53+
54+
@Transactional
55+
public Map.Entry<FileResource, byte[]> getDownload(ExtensionVersion extVersion) {
56+
var download = repositories.findFileByType(extVersion, FileResource.DOWNLOAD);
57+
var content = download.getStorageType().equals(FileResource.STORAGE_DB) ? download.getContent() : null;
58+
return new AbstractMap.SimpleEntry<>(download, content);
59+
}
60+
3961
@Retryable
4062
public Path getExtensionFile(Map.Entry<FileResource, byte[]> entry) {
4163
Path extensionFile;

server/src/main/java/org/eclipse/openvsx/migration/RenameDownloadsJobRequestHandler.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* ****************************************************************************** */
1010
package org.eclipse.openvsx.migration;
1111

12+
import org.eclipse.openvsx.entities.FileResource;
1213
import org.jobrunr.jobs.lambdas.JobRequestHandler;
1314
import org.slf4j.Logger;
1415
import org.slf4j.LoggerFactory;
@@ -20,7 +21,7 @@
2021
@Component
2122
public class RenameDownloadsJobRequestHandler implements JobRequestHandler<MigrationJobRequest> {
2223

23-
private static final Logger LOGGER = LoggerFactory.getLogger(RenameDownloadsJobRequestHandler.class);
24+
protected final Logger logger = LoggerFactory.getLogger(OrphanNamespaceMigration.class);
2425

2526
@Autowired
2627
MigrationService migrations;
@@ -30,14 +31,14 @@ public class RenameDownloadsJobRequestHandler implements JobRequestHandler<Migr
3031

3132
@Override
3233
public void run(MigrationJobRequest jobRequest) throws Exception {
33-
var download = service.getResource(jobRequest);
34+
var download = migrations.find(jobRequest, FileResource.class);
3435
var name = service.getNewBinaryName(download);
3536
if(download.getName().equals(name)) {
3637
// names are the same, nothing to do
3738
return;
3839
}
3940

40-
LOGGER.info("Renaming download {}", download.getName());
41+
logger.info("Renaming download {}", download.getName());
4142
var content = service.getContent(download);
4243
var extensionFile = migrations.getExtensionFile(new AbstractMap.SimpleEntry<>(download, content));
4344
var newDownload = service.cloneResource(download, name);
@@ -46,6 +47,6 @@ public void run(MigrationJobRequest jobRequest) throws Exception {
4647

4748
download.setName(name);
4849
service.updateResource(download);
49-
LOGGER.info("Updated download name to: {}", name);
50+
logger.info("Updated download name to: {}", name);
5051
}
5152
}

server/src/main/java/org/eclipse/openvsx/migration/RenameDownloadsService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ public FileResource cloneResource(FileResource resource, String name) {
5454
return clone;
5555
}
5656

57-
public FileResource getResource(MigrationJobRequest jobRequest) {
58-
return entityManager.find(FileResource.class, jobRequest.getEntityId());
59-
}
60-
6157
@Transactional
6258
public void updateResource(FileResource resource) {
6359
entityManager.merge(resource);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/** ******************************************************************************
2+
* Copyright (c) 2022 Precies. Software Ltd and others
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
* ****************************************************************************** */
10+
package org.eclipse.openvsx.migration;
11+
12+
import org.eclipse.openvsx.ExtensionProcessor;
13+
import org.eclipse.openvsx.entities.ExtensionVersion;
14+
import org.eclipse.openvsx.entities.FileResource;
15+
import org.jobrunr.jobs.annotations.Job;
16+
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
17+
import org.jobrunr.jobs.lambdas.JobRequestHandler;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import org.springframework.beans.factory.annotation.Autowired;
21+
import org.springframework.stereotype.Component;
22+
23+
import java.util.function.Consumer;
24+
25+
@Component
26+
public class SetContentTypeJobRequestHandler implements JobRequestHandler<MigrationJobRequest> {
27+
28+
protected final Logger logger = new JobRunrDashboardLogger(LoggerFactory.getLogger(ExtractResourcesJobRequestHandler.class));
29+
30+
@Autowired
31+
MigrationService migrations;
32+
33+
@Autowired
34+
SetContentTypeJobService service;
35+
36+
@Override
37+
@Job(name = "Set content type for published file resources", retries = 3)
38+
public void run(MigrationJobRequest jobRequest) throws Exception {
39+
var extVersion = migrations.find(jobRequest, ExtensionVersion.class);
40+
logger.info("Set content type for: {}.{}-{}@{}", extVersion.getExtension().getNamespace().getName(), extVersion.getExtension().getName(), extVersion.getVersion(), extVersion.getTargetPlatform());
41+
42+
var entry = migrations.getDownload(extVersion);
43+
var extensionFile = migrations.getExtensionFile(entry);
44+
try (var processor = new ExtensionProcessor(extensionFile)) {
45+
Consumer<FileResource> consumer = resource -> {
46+
service.setContentType(extVersion, resource);
47+
migrations.deleteResource(resource);
48+
migrations.uploadResource(resource);
49+
};
50+
51+
processor.getFileResources(extVersion).forEach(consumer);
52+
processor.processEachResource(extVersion, consumer);
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)