Skip to content

Commit 07082be

Browse files
authored
Add support for CDN front URL configuration for storage providers (#1452)
* add support for CDN front URL configuration for storage providers * remove check if storage service is enabled to support mirrors * clarify UrlUtil.createURI method * initialize service map by default * fix cdn javadoc wrt configuration
1 parent 435233a commit 07082be

File tree

12 files changed

+438
-230
lines changed

12 files changed

+438
-230
lines changed

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010

1111
package org.eclipse.openvsx.storage;
1212

13-
import org.apache.commons.lang3.ArrayUtils;
1413
import org.apache.commons.lang3.StringUtils;
1514
import org.eclipse.openvsx.cache.FilesCacheKeyGenerator;
1615
import org.eclipse.openvsx.entities.FileResource;
1716
import org.eclipse.openvsx.entities.Namespace;
1817
import org.eclipse.openvsx.util.FileUtil;
1918
import org.eclipse.openvsx.util.TempFile;
20-
import org.eclipse.openvsx.util.UrlUtil;
2119
import org.springframework.beans.factory.annotation.Value;
2220
import org.springframework.cache.annotation.Cacheable;
2321
import org.springframework.data.util.Pair;
@@ -305,22 +303,4 @@ public Path getCachedFile(FileResource resource) {
305303

306304
return path;
307305
}
308-
309-
protected String getObjectKey(FileResource resource) {
310-
var extVersion = resource.getExtension();
311-
var extension = extVersion.getExtension();
312-
var namespace = extension.getNamespace();
313-
var segments = new String[] {namespace.getName(), extension.getName()};
314-
if (!extVersion.isUniversalTargetPlatform()) {
315-
segments = ArrayUtils.add(segments, extVersion.getTargetPlatform());
316-
}
317-
318-
segments = ArrayUtils.add(segments, extVersion.getVersion());
319-
segments = ArrayUtils.addAll(segments, resource.getName().split("/"));
320-
return UrlUtil.createApiUrl("", segments).substring(1); // remove first '/'
321-
}
322-
323-
protected String getObjectKey(Namespace namespace) {
324-
return UrlUtil.createApiUrl("", namespace.getName(), "logo", namespace.getLogoName()).substring(1);
325-
}
326306
}

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

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@
1717
import com.azure.storage.blob.models.BlobHttpHeaders;
1818
import com.azure.storage.blob.models.BlobStorageException;
1919
import com.azure.storage.blob.models.CopyStatusType;
20-
import org.apache.commons.lang3.ArrayUtils;
2120
import org.apache.commons.lang3.StringUtils;
2221
import org.eclipse.openvsx.cache.FilesCacheKeyGenerator;
2322
import org.eclipse.openvsx.entities.FileResource;
2423
import org.eclipse.openvsx.entities.Namespace;
2524
import org.eclipse.openvsx.util.FileUtil;
2625
import org.eclipse.openvsx.util.TempFile;
27-
import org.eclipse.openvsx.util.UrlUtil;
2826
import org.springframework.beans.factory.annotation.Value;
2927
import org.springframework.cache.annotation.Cacheable;
3028
import org.springframework.data.util.Pair;
@@ -92,14 +90,14 @@ private String missingEndpointMessage(String action, String name) {
9290
@Override
9391
public void uploadFile(TempFile tempFile) {
9492
var resource = tempFile.getResource();
95-
var blobName = getBlobName(resource);
93+
var blobName = getObjectKey(resource);
9694
uploadFile(tempFile, resource.getName(), blobName);
9795
}
9896

9997
@Override
10098
public void uploadNamespaceLogo(TempFile logoFile) {
10199
var namespace = logoFile.getNamespace();
102-
var blobName = getBlobName(namespace);
100+
var blobName = getObjectKey(namespace);
103101
uploadFile(logoFile, namespace.getLogoName(), blobName);
104102
}
105103

@@ -124,12 +122,12 @@ protected void uploadFile(TempFile file, String fileName, String blobName) {
124122

125123
@Override
126124
public void removeFile(FileResource resource) {
127-
removeFile(getBlobName(resource));
125+
removeFile(getObjectKey(resource));
128126
}
129127

130128
@Override
131129
public void removeNamespaceLogo(Namespace namespace) {
132-
removeFile(getBlobName(namespace));
130+
removeFile(getObjectKey(namespace));
133131
}
134132

135133
private void removeFile(String blobName) {
@@ -150,7 +148,7 @@ private void removeFile(String blobName) {
150148

151149
@Override
152150
public URI getLocation(FileResource resource) {
153-
var blobName = getBlobName(resource);
151+
var blobName = getObjectKey(resource);
154152
if (StringUtils.isEmpty(serviceEndpoint)) {
155153
throw new IllegalStateException(missingEndpointMessage(blobName));
156154
}
@@ -160,23 +158,9 @@ public URI getLocation(FileResource resource) {
160158
return URI.create(serviceEndpoint + blobContainer + "/" + blobName);
161159
}
162160

163-
protected String getBlobName(FileResource resource) {
164-
var extVersion = resource.getExtension();
165-
var extension = extVersion.getExtension();
166-
var namespace = extension.getNamespace();
167-
var segments = new String[]{namespace.getName(), extension.getName()};
168-
if(!extVersion.isUniversalTargetPlatform()) {
169-
segments = ArrayUtils.add(segments, extVersion.getTargetPlatform());
170-
}
171-
172-
segments = ArrayUtils.add(segments, extVersion.getVersion());
173-
segments = ArrayUtils.addAll(segments, resource.getName().split("/"));
174-
return UrlUtil.createApiUrl("", segments).substring(1); // remove first '/'
175-
}
176-
177161
@Override
178162
public URI getNamespaceLogoLocation(Namespace namespace) {
179-
var blobName = getBlobName(namespace);
163+
var blobName = getObjectKey(namespace);
180164
if (StringUtils.isEmpty(serviceEndpoint)) {
181165
throw new IllegalStateException(missingEndpointMessage(blobName));
182166
}
@@ -188,7 +172,7 @@ public URI getNamespaceLogoLocation(Namespace namespace) {
188172

189173
@Override
190174
public TempFile downloadFile(FileResource resource) throws IOException {
191-
var blobName = getBlobName(resource);
175+
var blobName = getObjectKey(resource);
192176
if (StringUtils.isEmpty(serviceEndpoint)) {
193177
throw new IllegalStateException(missingEndpointMessage(blobName));
194178
}
@@ -199,16 +183,12 @@ public TempFile downloadFile(FileResource resource) throws IOException {
199183
return tempFile;
200184
}
201185

202-
protected String getBlobName(Namespace namespace) {
203-
return UrlUtil.createApiUrl("", namespace.getName(), "logo", namespace.getLogoName()).substring(1); // remove first '/'
204-
}
205-
206186
@Override
207187
public void copyFiles(List<Pair<FileResource,FileResource>> pairs) {
208188
var copyOperations = new ArrayList<SyncPoller<BlobCopyInfo, Void>>();
209189
for(var pair : pairs) {
210190
var oldLocation = getLocation(pair.getFirst()).toString();
211-
var newBlobName = getBlobName(pair.getSecond());
191+
var newBlobName = getObjectKey(pair.getSecond());
212192
var poller = getContainerClient().getBlobClient(newBlobName)
213193
.beginCopy(oldLocation, Duration.of(1, ChronoUnit.SECONDS));
214194

@@ -225,7 +205,7 @@ public void copyFiles(List<Pair<FileResource,FileResource>> pairs) {
225205
@Override
226206
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
227207
var oldLocation = getNamespaceLogoLocation(oldNamespace).toString();
228-
var newBlobName = getBlobName(newNamespace);
208+
var newBlobName = getObjectKey(newNamespace);
229209
var poller = getContainerClient().getBlobClient(newBlobName)
230210
.beginCopy(oldLocation, Duration.of(1, ChronoUnit.SECONDS));
231211

@@ -238,7 +218,7 @@ public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
238218
@Override
239219
@Cacheable(value = CACHE_EXTENSION_FILES, keyGenerator = GENERATOR_FILES, cacheManager = "fileCacheManager")
240220
public Path getCachedFile(FileResource resource) {
241-
var blobName = getBlobName(resource);
221+
var blobName = getObjectKey(resource);
242222
if (StringUtils.isEmpty(serviceEndpoint)) {
243223
throw new IllegalStateException(missingEndpointMessage(blobName));
244224
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/********************************************************************************
2+
* Copyright (c) 2025 Eclipse Foundation 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.storage;
11+
12+
import org.springframework.boot.context.properties.ConfigurationProperties;
13+
import org.springframework.context.annotation.Configuration;
14+
15+
import javax.annotation.Nullable;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
19+
/**
20+
* Supports configuring a CDN front URL for different storage providers.
21+
* <p>
22+
* If a CDN front URL is configured for a specific storage type, the URL locations
23+
* for file resources and namespace logos in {@link StorageUtilService} are returned as
24+
* {@code frontURL + "/" + objectKey(resource)}
25+
* <p>
26+
* Example config for AWS:
27+
* <pre>
28+
* ovsx:
29+
* storage:
30+
* cdn:
31+
* enabled: true
32+
* services:
33+
* aws: https://my.cdn.front.domain/
34+
* </pre>
35+
*/
36+
@Configuration
37+
@ConfigurationProperties("ovsx.storage.cdn")
38+
public class CdnServiceConfig {
39+
private boolean enabled;
40+
private Map<String, String> services = new HashMap<>();
41+
42+
public boolean isEnabled() {
43+
return enabled;
44+
}
45+
46+
public void setEnabled(boolean enabled) {
47+
this.enabled = enabled;
48+
}
49+
50+
public Map<String, String> getServices() {
51+
return services;
52+
}
53+
54+
public void setServices(Map<String, String> services) {
55+
this.services = services;
56+
}
57+
58+
public @Nullable String getCdnFrontUrl(String storageType) {
59+
return enabled ? services.get(storageType) : null;
60+
}
61+
}

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

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
package org.eclipse.openvsx.storage;
1111

1212
import com.google.cloud.storage.*;
13-
import org.apache.commons.lang3.ArrayUtils;
1413
import org.apache.commons.lang3.StringUtils;
1514
import org.eclipse.openvsx.cache.FilesCacheKeyGenerator;
1615
import org.eclipse.openvsx.entities.FileResource;
1716
import org.eclipse.openvsx.entities.Namespace;
1817
import org.eclipse.openvsx.util.FileUtil;
1918
import org.eclipse.openvsx.util.TempFile;
20-
import org.eclipse.openvsx.util.UrlUtil;
2119
import org.springframework.beans.factory.annotation.Value;
2220
import org.springframework.cache.annotation.Cacheable;
2321
import org.springframework.data.util.Pair;
@@ -76,7 +74,7 @@ protected Storage getStorage() {
7674
@Override
7775
public void uploadFile(TempFile tempFile) {
7876
var resource = tempFile.getResource();
79-
var objectId = getObjectId(resource);
77+
var objectId = getObjectKey(resource);
8078
if (StringUtils.isEmpty(bucketId)) {
8179
throw new IllegalStateException(missingBucketIdMessage("Cannot upload file", resource.getName()));
8280
}
@@ -87,7 +85,7 @@ public void uploadFile(TempFile tempFile) {
8785
@Override
8886
public void uploadNamespaceLogo(TempFile logoFile) {
8987
var namespace = logoFile.getNamespace();
90-
var objectId = getObjectId(namespace);
88+
var objectId = getObjectKey(namespace);
9189
if (StringUtils.isEmpty(bucketId)) {
9290
throw new IllegalStateException(missingBucketIdMessage("Cannot upload file", objectId));
9391
}
@@ -121,12 +119,12 @@ protected void uploadFile(TempFile file, String fileName, String objectId) {
121119

122120
@Override
123121
public void removeFile(FileResource resource) {
124-
removeFile(getObjectId(resource));
122+
removeFile(getObjectKey(resource));
125123
}
126124

127125
@Override
128126
public void removeNamespaceLogo(Namespace namespace) {
129-
removeFile(getObjectId(namespace));
127+
removeFile(getObjectKey(namespace));
130128
}
131129

132130
private void removeFile(String objectId) {
@@ -142,29 +140,15 @@ public URI getLocation(FileResource resource) {
142140
if (StringUtils.isEmpty(bucketId)) {
143141
throw new IllegalStateException(missingBucketIdMessage(resource.getName()));
144142
}
145-
return URI.create(BASE_URL + bucketId + "/" + getObjectId(resource));
146-
}
147-
148-
protected String getObjectId(FileResource resource) {
149-
var extVersion = resource.getExtension();
150-
var extension = extVersion.getExtension();
151-
var namespace = extension.getNamespace();
152-
var segments = new String[]{namespace.getName(), extension.getName()};
153-
if(!extVersion.isUniversalTargetPlatform()) {
154-
segments = ArrayUtils.add(segments, extVersion.getTargetPlatform());
155-
}
156-
157-
segments = ArrayUtils.add(segments, extVersion.getVersion());
158-
segments = ArrayUtils.addAll(segments, resource.getName().split("/"));
159-
return UrlUtil.createApiUrl("", segments).substring(1); // remove first '/'
143+
return URI.create(BASE_URL + bucketId + "/" + getObjectKey(resource));
160144
}
161145

162146
@Override
163147
public URI getNamespaceLogoLocation(Namespace namespace) {
164148
if (StringUtils.isEmpty(bucketId)) {
165149
throw new IllegalStateException(missingBucketIdMessage(namespace.getLogoName()));
166150
}
167-
return URI.create(BASE_URL + bucketId + "/" + getObjectId(namespace));
151+
return URI.create(BASE_URL + bucketId + "/" + getObjectKey(namespace));
168152
}
169153

170154
@Override
@@ -174,16 +158,12 @@ public TempFile downloadFile(FileResource resource) throws IOException {
174158
}
175159

176160
var tempFile = new TempFile("temp_file_", "");
177-
var objectId = getObjectId(resource);
161+
var objectId = getObjectKey(resource);
178162
getStorage().downloadTo(BlobId.of(bucketId, objectId), tempFile.getPath());
179163
tempFile.setResource(resource);
180164
return tempFile;
181165
}
182166

183-
protected String getObjectId(Namespace namespace) {
184-
return UrlUtil.createApiUrl("", namespace.getName(), "logo", namespace.getLogoName()).substring(1); // remove first '/'
185-
}
186-
187167
private String missingBucketIdMessage(String name) {
188168
return missingBucketIdMessage("Cannot determine location of file", name);
189169
}
@@ -194,12 +174,12 @@ private String missingBucketIdMessage(String action, String name) {
194174

195175
@Override
196176
public void copyFiles(List<Pair<FileResource,FileResource>> pairs) {
197-
pairs.forEach(pair -> copy(getObjectId(pair.getFirst()), getObjectId(pair.getSecond())));
177+
pairs.forEach(pair -> copy(getObjectKey(pair.getFirst()), getObjectKey(pair.getSecond())));
198178
}
199179

200180
@Override
201181
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
202-
copy(getObjectId(oldNamespace), getObjectId(newNamespace));
182+
copy(getObjectKey(oldNamespace), getObjectKey(newNamespace));
203183
}
204184

205185
private void copy(String source, String target) {
@@ -218,7 +198,7 @@ public Path getCachedFile(FileResource resource) {
218198
throw new IllegalStateException(missingBucketIdMessage(resource.getName()));
219199
}
220200

221-
var objectId = getObjectId(resource);
201+
var objectId = getObjectKey(resource);
222202
var path = filesCacheKeyGenerator.generateCachedExtensionPath(resource);
223203
FileUtil.writeSync(path, p -> getStorage().downloadTo(BlobId.of(bucketId, objectId), p));
224204
return path;

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
********************************************************************************/
1010
package org.eclipse.openvsx.storage;
1111

12+
import org.apache.commons.lang3.ArrayUtils;
1213
import org.eclipse.openvsx.entities.FileResource;
1314
import org.eclipse.openvsx.entities.Namespace;
1415
import org.eclipse.openvsx.util.TempFile;
16+
import org.eclipse.openvsx.util.UrlUtil;
1517
import org.springframework.data.util.Pair;
1618

19+
import javax.annotation.Nullable;
1720
import java.io.IOException;
1821
import java.net.URI;
1922
import java.nio.file.Path;
@@ -62,5 +65,25 @@ public interface IStorageService {
6265

6366
void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace);
6467

65-
Path getCachedFile(FileResource resource);
68+
@Nullable Path getCachedFile(FileResource resource);
69+
70+
default String getObjectKey(FileResource resource) {
71+
var extVersion = resource.getExtension();
72+
var extension = extVersion.getExtension();
73+
var namespace = extension.getNamespace();
74+
var segments = new String[]{namespace.getName(), extension.getName()};
75+
if(!extVersion.isUniversalTargetPlatform()) {
76+
segments = ArrayUtils.add(segments, extVersion.getTargetPlatform());
77+
}
78+
79+
segments = ArrayUtils.add(segments, extVersion.getVersion());
80+
segments = ArrayUtils.addAll(segments, resource.getName().split("/"));
81+
var url = UrlUtil.createApiUrl("", segments);
82+
return url != null ? url.substring(1) : null; // remove first '/'
83+
}
84+
85+
default String getObjectKey(Namespace namespace) {
86+
var url = UrlUtil.createApiUrl("", namespace.getName(), "logo", namespace.getLogoName());
87+
return url != null ? url.substring(1) : null; // remove first '/'
88+
}
6689
}

0 commit comments

Comments
 (0)