Skip to content

Commit 0a5dbba

Browse files
committed
Web extension resources return incorrect MIME type
Use [Content_Types].xml to set FileResource contentType
1 parent f956110 commit 0a5dbba

31 files changed

+734
-98
lines changed

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

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

12-
import java.io.EOFException;
13-
import java.io.File;
14-
import java.io.IOException;
15-
import java.io.InputStream;
12+
import java.io.*;
1613
import java.util.*;
1714
import java.util.function.Consumer;
1815
import java.util.regex.Pattern;
@@ -40,6 +37,11 @@
4037
import org.slf4j.LoggerFactory;
4138
import org.springframework.data.util.Pair;
4239
import org.springframework.http.HttpStatus;
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;
4345

4446
/**
4547
* Processes uploaded extension files and extracts their metadata.
@@ -301,6 +303,7 @@ public List<FileResource> getResources(ExtensionVersion extVersion) {
301303

302304
public List<FileResource> getResources(ExtensionVersion extVersion, List<String> excludes) {
303305
var resources = new ArrayList<FileResource>();
306+
var contentTypes = loadContentTypes(resources);
304307
if(!excludes.contains(FileResource.RESOURCE)) {
305308
resources.addAll(getAllResources(extVersion).collect(Collectors.toList()));
306309
}
@@ -335,7 +338,9 @@ public List<FileResource> getResources(ExtensionVersion extVersion, List<String>
335338
resources.add(icon);
336339
}
337340

338-
return resources;
341+
return resources.stream()
342+
.map(resource -> setContentType(resource, contentTypes))
343+
.collect(Collectors.toList());
339344
}
340345

341346
public void processEachResource(ExtensionVersion extVersion, Consumer<FileResource> processor) {
@@ -461,6 +466,48 @@ public FileResource getLicense(ExtensionVersion extVersion) {
461466
return license;
462467
}
463468

469+
private Map<String, String> loadContentTypes(List<FileResource> resources) {
470+
var contentTypes = resources.stream()
471+
.filter(r -> r.getName().equals("[Content_Types].xml"))
472+
.findFirst()
473+
.map(FileResource::getContent)
474+
.map(this::parseContentTypesXml)
475+
.orElse(new HashMap<>());
476+
477+
contentTypes.putIfAbsent(".vsix", "application/zip");
478+
return contentTypes;
479+
}
480+
481+
private Map<String, String> parseContentTypesXml(byte[] content) {
482+
try (var input = new ByteArrayInputStream(content)) {
483+
var contentTypes = new HashMap<String, String>();
484+
var document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(input);
485+
var elements = document.getDocumentElement().getElementsByTagName("Default");
486+
for(var i = 0; i < elements.getLength(); i++) {
487+
var element = elements.item(i);
488+
var attributes = element.getAttributes();
489+
var extension = attributes.getNamedItem("Extension").getTextContent();
490+
var contentType = attributes.getNamedItem("ContentType").getTextContent();
491+
contentTypes.put(extension, contentType);
492+
}
493+
494+
return contentTypes;
495+
} catch (IOException | ParserConfigurationException | SAXException e) {
496+
logger.error("failed to read content types", e);
497+
return null;
498+
}
499+
}
500+
501+
private FileResource setContentType(FileResource resource, Map<String, String> contentTypes) {
502+
var resourceName = Optional.ofNullable(resource.getName()).orElse("");
503+
var fileExtensionIndex = resourceName.lastIndexOf('.');
504+
var fileExtension = fileExtensionIndex != -1 ? resourceName.substring(fileExtensionIndex) : "";
505+
var contentType = contentTypes.getOrDefault(fileExtension, MediaType.APPLICATION_OCTET_STREAM_VALUE);
506+
507+
resource.setContentType(contentType);
508+
return resource;
509+
}
510+
464511
private void detectLicense(byte[] content, ExtensionVersion extVersion) {
465512
if (Strings.isNullOrEmpty(extVersion.getLicense())) {
466513
var detection = new LicenseDetection();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public ResponseEntity<byte[]> getFile(String namespace, String extensionName, St
173173
if (resource.getType().equals(DOWNLOAD))
174174
storageUtil.increaseDownloadCount(extVersion, resource);
175175
if (resource.getStorageType().equals(FileResource.STORAGE_DB)) {
176-
var headers = storageUtil.getFileResponseHeaders(fileName);
176+
var headers = storageUtil.getFileResponseHeaders(resource);
177177
return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK);
178178
} else {
179179
return ResponseEntity.status(HttpStatus.FOUND)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ public ResponseEntity<byte[]> getAsset(
304304
storageUtil.increaseDownloadCount(extVersion, resource);
305305
}
306306
if (resource.getStorageType().equals(FileResource.STORAGE_DB)) {
307-
var headers = storageUtil.getFileResponseHeaders(resource.getName());
307+
var headers = storageUtil.getFileResponseHeaders(resource);
308308
return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK);
309309
} else {
310310
return ResponseEntity.status(HttpStatus.FOUND)
@@ -416,7 +416,7 @@ private ResponseEntity<byte[]> browseFile(
416416
String version
417417
) {
418418
if (resource.getStorageType().equals(FileResource.STORAGE_DB)) {
419-
var headers = storageUtil.getFileResponseHeaders(resource.getName());
419+
var headers = storageUtil.getFileResponseHeaders(resource);
420420
return new ResponseEntity<>(resource.getContent(), headers, HttpStatus.OK);
421421
} else {
422422
var namespace = new Namespace();

server/src/main/java/org/eclipse/openvsx/dto/FileResourceDTO.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111

1212
import org.eclipse.openvsx.entities.FileResource;
1313

14-
import java.util.Objects;
15-
1614
public class FileResourceDTO {
1715

1816
private final long extensionVersionId;
@@ -23,11 +21,21 @@ public class FileResourceDTO {
2321
private final String type;
2422
private String storageType;
2523
private byte[] content;
26-
27-
public FileResourceDTO(long id, long extensionVersionId, String name, String type, String storageType, byte[] content) {
24+
private String contentType;
25+
26+
public FileResourceDTO(
27+
long id,
28+
long extensionVersionId,
29+
String name,
30+
String type,
31+
String storageType,
32+
byte[] content,
33+
String contentType
34+
) {
2835
this(id, extensionVersionId, name, type);
2936
this.storageType = storageType;
3037
this.content = content;
38+
this.contentType = contentType;
3139
}
3240

3341
public FileResourceDTO(long id, long extensionVersionId, String name, String type) {
@@ -73,6 +81,8 @@ public byte[] getContent() {
7381
return content;
7482
}
7583

84+
public String getContentType() { return contentType; }
85+
7686
public boolean isWebResource() {
7787
return type.equals(FileResource.RESOURCE) && name.startsWith("extension/");
7888
}

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

@@ -89,6 +91,14 @@ public void setContent(byte[] content) {
8991
this.content = content;
9092
}
9193

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

server/src/main/java/org/eclipse/openvsx/repositories/FileResourceDTORepository.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
import org.eclipse.openvsx.entities.FileResource;
1414
import org.jooq.DSLContext;
1515
import org.springframework.beans.factory.annotation.Autowired;
16-
import org.springframework.data.jpa.repository.Query;
17-
import org.springframework.data.repository.Repository;
18-
import org.springframework.data.util.Streamable;
1916
import org.springframework.stereotype.Component;
2017

2118
import java.util.Collection;
@@ -30,15 +27,28 @@ public class FileResourceDTORepository {
3027
DSLContext dsl;
3128

3229
public List<FileResourceDTO> findAll(Collection<Long> extensionIds, Collection<String> types) {
33-
return dsl.select(FILE_RESOURCE.ID, FILE_RESOURCE.EXTENSION_ID, FILE_RESOURCE.NAME, FILE_RESOURCE.TYPE)
30+
return dsl.select(
31+
FILE_RESOURCE.ID,
32+
FILE_RESOURCE.EXTENSION_ID,
33+
FILE_RESOURCE.NAME,
34+
FILE_RESOURCE.TYPE
35+
)
3436
.from(FILE_RESOURCE)
3537
.where(FILE_RESOURCE.EXTENSION_ID.in(extensionIds))
3638
.and(FILE_RESOURCE.TYPE.in(types))
3739
.fetchInto(FileResourceDTO.class);
3840
}
3941

4042
public List<FileResourceDTO> findAllResources(long extVersionId, String prefix) {
41-
return dsl.select(FILE_RESOURCE.ID, FILE_RESOURCE.EXTENSION_ID, FILE_RESOURCE.NAME, FILE_RESOURCE.TYPE, FILE_RESOURCE.STORAGE_TYPE, FILE_RESOURCE.CONTENT)
43+
return dsl.select(
44+
FILE_RESOURCE.ID,
45+
FILE_RESOURCE.EXTENSION_ID,
46+
FILE_RESOURCE.NAME,
47+
FILE_RESOURCE.TYPE,
48+
FILE_RESOURCE.STORAGE_TYPE,
49+
FILE_RESOURCE.CONTENT,
50+
FILE_RESOURCE.CONTENT_TYPE
51+
)
4252
.from(FILE_RESOURCE)
4353
.where(FILE_RESOURCE.TYPE.eq(FileResource.RESOURCE))
4454
.and(FILE_RESOURCE.EXTENSION_ID.eq(extVersionId))

server/src/main/java/org/eclipse/openvsx/storage/AzureBlobStorageService.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,21 @@ public void uploadFile(FileResource resource) {
6464
+ blobName + ": missing Azure blob service endpoint");
6565
}
6666

67-
uploadFile(resource.getContent(), resource.getName(), blobName);
67+
uploadFile(resource, blobName);
6868
}
6969

70-
protected void uploadFile(byte[] content, String fileName, String blobName) {
70+
protected void uploadFile(FileResource resource, String blobName) {
7171
var blobClient = getContainerClient().getBlobClient(blobName);
7272
var headers = new BlobHttpHeaders();
73-
headers.setContentType(StorageUtil.getFileType(fileName).toString());
74-
if (fileName.endsWith(".vsix")) {
75-
headers.setContentDisposition("attachment; filename=\"" + fileName + "\"");
73+
headers.setContentType(resource.getContentType());
74+
if (resource.getName().endsWith(".vsix")) {
75+
headers.setContentDisposition("attachment; filename=\"" + resource.getName() + "\"");
7676
} else {
77-
var cacheControl = StorageUtil.getCacheControl(fileName);
77+
var cacheControl = StorageUtil.getCacheControl(resource.getName());
7878
headers.setCacheControl(cacheControl.getHeaderValue());
7979
}
80+
81+
var content = resource.getContent();
8082
try (var dataStream = new ByteArrayInputStream(content)) {
8183
blobClient.upload(dataStream, content.length, true);
8284
blobClient.setHttpHeaders(headers);

server/src/main/java/org/eclipse/openvsx/storage/GoogleCloudStorageService.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,19 @@ public void uploadFile(FileResource resource) {
6464
+ objectId + ": missing Google bucket id");
6565
}
6666

67-
uploadFile(resource.getContent(), resource.getName(), objectId);
67+
uploadFile(resource, objectId);
6868
}
6969

70-
protected void uploadFile(byte[] content, String fileName, String objectId) {
70+
protected void uploadFile(FileResource resource, String objectId) {
7171
var blobInfoBuilder = BlobInfo.newBuilder(BlobId.of(bucketId, objectId))
72-
.setContentType(StorageUtil.getFileType(fileName).toString());
73-
if (fileName.endsWith(".vsix")) {
74-
blobInfoBuilder.setContentDisposition("attachment; filename=\"" + fileName + "\"");
72+
.setContentType(resource.getContentType());
73+
if (resource.getName().endsWith(".vsix")) {
74+
blobInfoBuilder.setContentDisposition("attachment; filename=\"" + resource.getName() + "\"");
7575
} else {
76-
var cacheControl = StorageUtil.getCacheControl(fileName);
76+
var cacheControl = StorageUtil.getCacheControl(resource.getName());
7777
blobInfoBuilder.setCacheControl(cacheControl.getHeaderValue());
7878
}
79-
getStorage().create(blobInfoBuilder.build(), content);
79+
getStorage().create(blobInfoBuilder.build(), resource.getContent());
8080
}
8181

8282
@Override

server/src/main/java/org/eclipse/openvsx/storage/StorageUtil.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,13 @@
1111
package org.eclipse.openvsx.storage;
1212

1313
import org.springframework.http.CacheControl;
14-
import org.springframework.http.MediaType;
1514

16-
import java.net.URLConnection;
1715
import java.util.concurrent.TimeUnit;
1816

1917
class StorageUtil {
2018

2119
private StorageUtil(){}
2220

23-
static MediaType getFileType(String fileName) {
24-
if (fileName.endsWith(".vsix"))
25-
return MediaType.APPLICATION_OCTET_STREAM;
26-
if (fileName.endsWith(".json"))
27-
return MediaType.APPLICATION_JSON;
28-
var contentType = URLConnection.guessContentTypeFromName(fileName);
29-
if (contentType != null)
30-
return MediaType.parseMediaType(contentType);
31-
return MediaType.TEXT_PLAIN;
32-
}
33-
3421
static CacheControl getCacheControl(String fileName) {
3522
// Files are requested with a version string in the URL, so their content cannot change
3623
return CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic();

server/src/main/java/org/eclipse/openvsx/storage/StorageUtilService.java

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

1212
import com.google.common.base.Strings;
13+
14+
import org.eclipse.openvsx.dto.FileResourceDTO;
1315
import com.google.common.collect.Maps;
1416
import org.eclipse.openvsx.entities.ExtensionVersion;
1517
import org.eclipse.openvsx.entities.FileResource;
@@ -19,6 +21,7 @@
1921
import org.springframework.beans.factory.annotation.Autowired;
2022
import org.springframework.beans.factory.annotation.Value;
2123
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.MediaType;
2225
import org.springframework.stereotype.Component;
2326

2427
import javax.transaction.Transactional;
@@ -170,9 +173,17 @@ public void increaseDownloadCount(ExtensionVersion extVersion, FileResource reso
170173
downloadCountService.increaseDownloadCount(extVersion, resource, List.of(TimeUtil.getCurrentUTC()));
171174
}
172175

173-
public HttpHeaders getFileResponseHeaders(String fileName) {
176+
public HttpHeaders getFileResponseHeaders(FileResource resource) {
177+
return getFileResponseHeaders(resource.getName(), resource.getContentType());
178+
}
179+
180+
public HttpHeaders getFileResponseHeaders(FileResourceDTO resource) {
181+
return getFileResponseHeaders(resource.getName(), resource.getContentType());
182+
}
183+
184+
private HttpHeaders getFileResponseHeaders(String fileName, String contentType) {
174185
var headers = new HttpHeaders();
175-
headers.setContentType(StorageUtil.getFileType(fileName));
186+
headers.setContentType(MediaType.parseMediaType(contentType));
176187
if (fileName.endsWith(".vsix")) {
177188
headers.add("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
178189
} else {

0 commit comments

Comments
 (0)