Skip to content

Commit 180420d

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 180420d

File tree

61 files changed

+5598
-253
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+5598
-253
lines changed

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

Lines changed: 58 additions & 14 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;
@@ -26,6 +27,7 @@
2627
import com.fasterxml.jackson.databind.node.MissingNode;
2728
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
2829

30+
import org.apache.commons.io.FilenameUtils;
2931
import org.eclipse.openvsx.entities.ExtensionVersion;
3032
import org.eclipse.openvsx.entities.FileResource;
3133
import org.eclipse.openvsx.util.ArchiveUtil;
@@ -36,6 +38,11 @@
3638
import org.slf4j.Logger;
3739
import org.slf4j.LoggerFactory;
3840
import org.springframework.data.util.Pair;
41+
import org.springframework.http.MediaType;
42+
import org.xml.sax.SAXException;
43+
44+
import javax.xml.parsers.DocumentBuilderFactory;
45+
import javax.xml.parsers.ParserConfigurationException;
3946

4047
/**
4148
* Processes uploaded extension files and extracts their metadata.
@@ -282,17 +289,22 @@ private List<String> getEngines(JsonNode node) {
282289
}
283290

284291
public List<FileResource> getFileResources(ExtensionVersion extVersion) {
285-
var resources = new ArrayList<FileResource>();
292+
readInputStream();
293+
var contentTypes = loadContentTypes();
286294
var mappers = List.<Function<ExtensionVersion, FileResource>>of(
287295
this::getManifest, this::getReadme, this::getChangelog, this::getLicense, this::getIcon
288296
);
289297

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

294305
public void processEachResource(ExtensionVersion extVersion, Consumer<FileResource> processor) {
295306
readInputStream();
307+
var contentTypes = loadContentTypes();
296308
zipFile.stream()
297309
.filter(zipEntry -> !zipEntry.isDirectory())
298310
.map(zipEntry -> {
@@ -311,6 +323,7 @@ public void processEachResource(ExtensionVersion extVersion, Consumer<FileResour
311323
resource.setName(zipEntry.getName());
312324
resource.setType(FileResource.RESOURCE);
313325
resource.setContent(bytes);
326+
setContentType(resource, contentTypes);
314327
return resource;
315328
})
316329
.filter(Objects::nonNull)
@@ -393,9 +406,7 @@ public FileResource getLicense(ExtensionVersion extVersion) {
393406
var fileName = matcher.group("file");
394407
var bytes = ArchiveUtil.readEntry(zipFile, "extension/" + fileName);
395408
if (bytes != null) {
396-
var lastSegmentIndex = fileName.lastIndexOf('/');
397-
var lastSegment = fileName.substring(lastSegmentIndex + 1);
398-
license.setName(lastSegment);
409+
license.setName(FilenameUtils.getName(fileName));
399410
license.setContent(bytes);
400411
detectLicense(bytes, extVersion);
401412
return license;
@@ -413,6 +424,44 @@ 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 fileExtension = FilenameUtils.getExtension(resource.getName());
460+
var contentType = contentTypes.getOrDefault(fileExtension, MediaType.APPLICATION_OCTET_STREAM_VALUE);
461+
resource.setContentType(contentType);
462+
return resource;
463+
}
464+
416465
private void detectLicense(byte[] content, ExtensionVersion extVersion) {
417466
if (Strings.isNullOrEmpty(extVersion.getLicense())) {
418467
var detection = new LicenseDetection();
@@ -425,9 +474,7 @@ private Pair<byte[], String> readFromAlternateNames(String[] names) {
425474
var entry = ArchiveUtil.getEntryIgnoreCase(zipFile, name);
426475
if (entry != null) {
427476
var bytes = ArchiveUtil.readEntry(zipFile, entry);
428-
var lastSegmentIndex = entry.getName().lastIndexOf('/');
429-
var lastSegment = entry.getName().substring(lastSegmentIndex + 1);
430-
return Pair.of(bytes, lastSegment);
477+
return Pair.of(bytes, FilenameUtils.getName(entry.getName()));
431478
}
432479
}
433480
return null;
@@ -442,13 +489,10 @@ protected FileResource getIcon(ExtensionVersion extVersion) {
442489
var bytes = ArchiveUtil.readEntry(zipFile, "extension/" + iconPathStr);
443490
if (bytes == null)
444491
return null;
492+
445493
var icon = new FileResource();
446494
icon.setExtension(extVersion);
447-
var fileNameIndex = iconPathStr.lastIndexOf('/');
448-
if (fileNameIndex >= 0)
449-
icon.setName(iconPathStr.substring(fileNameIndex + 1));
450-
else
451-
icon.setName(iconPathStr);
495+
icon.setName(FilenameUtils.getName(iconPathStr));
452496
icon.setType(FileResource.ICON);
453497
icon.setContent(bytes);
454498
return icon;

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
import java.util.concurrent.TimeUnit;
3232
import java.util.stream.Collectors;
3333

34-
import javax.transaction.Transactional;
35-
3634
import static org.eclipse.openvsx.adapter.ExtensionQueryParam.Criterion.*;
3735
import static org.eclipse.openvsx.adapter.ExtensionQueryParam.*;
3836
import static org.eclipse.openvsx.adapter.ExtensionQueryResult.Extension.FLAG_PREVIEW;
@@ -404,7 +402,7 @@ private ResponseEntity<byte[]> browseFile(
404402
String version
405403
) {
406404
if (resource.getStorageType().equals(FileResource.STORAGE_DB)) {
407-
var headers = storageUtil.getFileResponseHeaders(resource.getName());
405+
var headers = storageUtil.getFileResponseHeaders(resource);
408406
return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK);
409407
} else {
410408
var namespace = new Namespace();

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

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

12-
import org.hibernate.annotations.Fetch;
13-
1412
import javax.persistence.*;
1513

1614
@Entity
@@ -45,6 +43,8 @@ public class FileResource {
4543
@Basic(fetch = FetchType.LAZY)
4644
byte[] content;
4745

46+
String contentType;
47+
4848
@Column(length = 32)
4949
String storageType;
5050

@@ -88,6 +88,14 @@ public void setContent(byte[] content) {
8888
this.content = content;
8989
}
9090

91+
public String getContentType() {
92+
return contentType;
93+
}
94+
95+
public void setContentType(String contentType) {
96+
this.contentType = contentType;
97+
}
98+
9199
public String getStorageType() {
92100
return storageType;
93101
}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
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;
14-
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
1515
import org.jobrunr.jobs.lambdas.JobRequestHandler;
1616
import org.slf4j.Logger;
1717
import org.slf4j.LoggerFactory;
@@ -21,7 +21,7 @@
2121
@Component
2222
public class ExtractResourcesJobRequestHandler implements JobRequestHandler<MigrationJobRequest> {
2323

24-
protected final Logger logger = new JobRunrDashboardLogger(LoggerFactory.getLogger(ExtractResourcesJobRequestHandler.class));
24+
protected final Logger logger = LoggerFactory.getLogger(ExtractResourcesJobRequestHandler.class);
2525

2626
@Autowired
2727
ExtractResourcesJobService service;
@@ -32,12 +32,12 @@ public class ExtractResourcesJobRequestHandler implements JobRequestHandler<Migr
3232
@Override
3333
@Job(name = "Extract resources from published extension version", retries = 3)
3434
public void run(MigrationJobRequest jobRequest) throws Exception {
35-
var extVersion = service.getExtension(jobRequest.getEntityId());
35+
var extVersion = migrations.find(jobRequest, ExtensionVersion.class);
3636
logger.info("Extracting resources for: {}.{}-{}@{}", extVersion.getExtension().getNamespace().getName(), extVersion.getExtension().getName(), extVersion.getVersion(), extVersion.getTargetPlatform());
3737

3838
service.deleteResources(extVersion);
39-
var entry = service.getDownload(extVersion);
40-
var extensionFile = migrations.getExtensionFile(entry);
39+
var entry = migrations.getDownload(extVersion);
40+
var extensionFile = migrations.getFile(entry);
4141
var download = entry.getKey();
4242
try(var extProcessor = new ExtensionProcessor(extensionFile)) {
4343
extProcessor.processEachResource(download.getExtension(), (resource) -> {
@@ -48,5 +48,6 @@ public void run(MigrationJobRequest jobRequest) throws Exception {
4848
}
4949

5050
service.deleteWebResources(extVersion);
51+
migrations.deleteFile(extensionFile);
5152
}
5253
}

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

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,21 @@
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;
29-
import java.util.AbstractMap;
30-
import java.util.Map;
3120

3221
@Component
3322
public class ExtractResourcesJobService {
3423

3524
@Autowired
3625
RepositoryService repositories;
3726

38-
@Autowired
39-
RestTemplate restTemplate;
40-
4127
@Autowired
4228
EntityManager entityManager;
4329

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-
5430
@Transactional
5531
public void deleteResources(ExtensionVersion extVersion) {
5632
repositories.deleteFileResources(extVersion, "resource");
@@ -61,13 +37,6 @@ public void deleteWebResources(ExtensionVersion extVersion) {
6137
repositories.deleteFileResources(extVersion, "web-resource");
6238
}
6339

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-
7140
@Transactional
7241
public void persistResource(FileResource resource) {
7342
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));

0 commit comments

Comments
 (0)