Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix change namespace action #1065

Merged
merged 1 commit into from
Dec 9, 2024
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
1 change: 0 additions & 1 deletion server/src/main/java/org/eclipse/openvsx/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ public ResponseEntity<ResultJson> updateNamespaceDetailsLogo(
) {
try {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic())
.body(users.updateNamespaceDetailsLogo(namespace, file));
} catch (ErrorResultException exc) {
return exc.toResponseEntity(ResultJson.class);
Expand Down
18 changes: 11 additions & 7 deletions server/src/main/java/org/eclipse/openvsx/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

Expand Down Expand Up @@ -237,6 +238,12 @@ public ResultJson updateNamespaceDetails(NamespaceDetailsJson details) {
if(!Objects.equals(details.getSocialLinks(), namespace.getSocialLinks())) {
namespace.setSocialLinks(details.getSocialLinks());
}
if(StringUtils.isEmpty(details.getLogo()) && StringUtils.isNotEmpty(namespace.getLogoName())) {
storageUtil.removeNamespaceLogo(namespace);
namespace.clearLogoBytes();
namespace.setLogoName(null);
namespace.setLogoStorageType(null);
}

return ResultJson.success("Updated details for namespace " + details.getName());
}
Expand All @@ -257,15 +264,12 @@ public ResultJson updateNamespaceDetailsLogo(String namespaceName, MultipartFile
var tika = new Tika();
var detectedType = tika.detect(file.getInputStream(), file.getOriginalFilename());
var logoType = MimeTypes.getDefaultMimeTypes().getRegisteredMimeType(detectedType);
if(logoType != null) {
if(!logoType.getType().equals(MediaType.image("png")) && !logoType.getType().equals(MediaType.image("jpg"))) {
throw new ErrorResultException("Namespace logo should be of png or jpg type");
}

var logoName = "logo-" + namespace.getName() + "-" + System.currentTimeMillis() + logoType.getExtension();
namespace.setLogoName(logoName);
var expectedLogoTypes = List.of(MediaType.image("png"), MediaType.image("jpg"));
if(logoType == null || !expectedLogoTypes.contains(logoType.getType())) {
throw new ErrorResultException("Namespace logo should be a png or jpg file");
}

namespace.setLogoName(NamingUtil.toLogoName(namespace, logoType));
file.getInputStream().transferTo(out);
logoFile.setNamespace(namespace);
storageUtil.uploadNamespaceLogo(logoFile);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* ****************************************************************************** */
package org.eclipse.openvsx.admin;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.openvsx.ExtensionValidator;
import org.eclipse.openvsx.entities.Extension;
import org.eclipse.openvsx.entities.ExtensionVersion;
Expand Down Expand Up @@ -104,6 +105,11 @@ private void execute(ChangeNamespaceJobRequest jobRequest) {
})
.collect(Collectors.toList());

if(StringUtils.isNotEmpty(oldNamespace.getLogoName())) {
newNamespace.setLogoName(NamingUtil.changeLogoName(oldNamespace, newNamespace));
storageUtil.copyNamespaceLogo(oldNamespace, newNamespace);
}

service.changeNamespaceInDatabase(newNamespace, oldNamespace, updatedResources, createNewNamespace, json.removeOldNamespace());

// remove the old resources from external storage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public void changeNamespaceInDatabase(
entityManager.remove(oldNamespace);
}

cache.evictSitemap();
cache.evictNamespaceDetails(oldNamespace);
search.updateSearchEntries(extensions.toList());
}

Expand Down
18 changes: 13 additions & 5 deletions server/src/main/java/org/eclipse/openvsx/cache/CacheService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@
* ****************************************************************************** */
package org.eclipse.openvsx.cache;

import org.eclipse.openvsx.entities.Extension;
import org.eclipse.openvsx.entities.ExtensionVersion;
import org.eclipse.openvsx.entities.FileResource;
import org.eclipse.openvsx.entities.UserData;
import org.eclipse.openvsx.entities.*;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.util.TargetPlatform;
import org.eclipse.openvsx.util.VersionAlias;
Expand Down Expand Up @@ -59,17 +56,28 @@ public CacheService(
this.filesCacheKeyGenerator = filesCacheKeyGenerator;
}

public void evictSitemap() {
invalidateCache(CACHE_SITEMAP);
}

public void evictNamespaceDetails() {
invalidateCache(CACHE_NAMESPACE_DETAILS_JSON);
}

public void evictNamespaceDetails(Namespace namespace) {
evictNamespaceDetails(namespace.getName());
}

public void evictNamespaceDetails(Extension extension) {
evictNamespaceDetails(extension.getNamespace().getName());
}

private void evictNamespaceDetails(String namespaceName) {
var cache = cacheManager.getCache(CACHE_NAMESPACE_DETAILS_JSON);
if(cache == null) {
return; // cache is not created
}

var namespaceName = extension.getNamespace().getName();
cache.evictIfPresent(namespaceName);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,23 @@ public TempFile downloadFile(FileResource resource) throws IOException {

@Override
public void copyFiles(List<Pair<FileResource, FileResource>> pairs) {
for(var pair : pairs) {
var oldObjectKey = getObjectKey(pair.getFirst());
var newObjectKey = getObjectKey(pair.getSecond());
var request = CopyObjectRequest.builder()
.sourceBucket(bucket)
.sourceKey(oldObjectKey)
.destinationBucket(bucket)
.destinationKey(newObjectKey)
.build();
pairs.forEach(pair -> copy(getObjectKey(pair.getFirst()), getObjectKey(pair.getSecond())));
}

getS3Client().copyObject(request);
}
@Override
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
copy(getObjectKey(oldNamespace), getObjectKey(newNamespace));
}

private void copy(String oldObjectKey, String newObjectKey) {
var request = CopyObjectRequest.builder()
.sourceBucket(bucket)
.sourceKey(oldObjectKey)
.destinationBucket(bucket)
.destinationKey(newObjectKey)
.build();

getS3Client().copyObject(request);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@ public void copyFiles(List<Pair<FileResource,FileResource>> pairs) {
}
}

@Override
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
var oldLocation = getNamespaceLogoLocation(oldNamespace).toString();
var newBlobName = getBlobName(newNamespace);
var poller = getContainerClient().getBlobClient(newBlobName)
.beginCopy(oldLocation, Duration.of(1, ChronoUnit.SECONDS));

var response = poller.waitForCompletion();
if(response.getValue().getCopyStatus() != CopyStatusType.SUCCESS) {
throw new RuntimeException(response.getValue().getError());
}
}

@Override
@Cacheable(value = CACHE_EXTENSION_FILES, keyGenerator = GENERATOR_FILES)
public Path getCachedFile(FileResource resource) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,21 @@ private String missingBucketIdMessage(String action, String name) {

@Override
public void copyFiles(List<Pair<FileResource,FileResource>> pairs) {
for(var pair : pairs) {
var source = getObjectId(pair.getFirst());
var target = getObjectId(pair.getSecond());
var request = new Storage.CopyRequest.Builder()
.setSource(BlobId.of(bucketId, source))
.setTarget(BlobId.of(bucketId, target))
.build();

getStorage().copy(request).getResult();
}
pairs.forEach(pair -> copy(getObjectId(pair.getFirst()), getObjectId(pair.getSecond())));
}

@Override
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
copy(getObjectId(oldNamespace), getObjectId(newNamespace));
}

private void copy(String source, String target) {
var request = new Storage.CopyRequest.Builder()
.setSource(BlobId.of(bucketId, source))
.setTarget(BlobId.of(bucketId, target))
.build();

getStorage().copy(request).getResult();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,7 @@ public interface IStorageService {

void copyFiles(List<Pair<FileResource, FileResource>> pairs);

void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace);

Path getCachedFile(FileResource resource) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,22 @@ public TempFile downloadFile(FileResource resource) throws IOException {
public void copyFiles(List<Pair<FileResource, FileResource>> pairs) {
try {
for (var pair : pairs) {
var source = getPath(pair.getFirst());
var target = getPath(pair.getSecond());
Files.copy(source, target);
Files.copy(getPath(pair.getFirst()), getPath(pair.getSecond()), StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
try {
Files.copy(getLogoPath(oldNamespace), getLogoPath(newNamespace), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private Path getPath(FileResource resource) {
if(!isEnabled()) {
throw new IllegalStateException("Cannot determine location of file. Configure the 'ovsx.storage.local.directory' property.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,26 @@ public void copyFiles(List<Pair<FileResource,FileResource>> pairs) {
}
}

@Override
public void copyNamespaceLogo(Namespace oldNamespace, Namespace newNamespace) {
switch (oldNamespace.getLogoStorageType()) {
case STORAGE_GOOGLE:
googleStorage.copyNamespaceLogo(oldNamespace, newNamespace);
break;
case STORAGE_AZURE:
azureStorage.copyNamespaceLogo(oldNamespace, newNamespace);
break;
case STORAGE_AWS:
awsStorage.copyNamespaceLogo(oldNamespace, newNamespace);
break;
case STORAGE_LOCAL:
localStorage.copyNamespaceLogo(oldNamespace, newNamespace);
break;
}

newNamespace.setLogoStorageType(oldNamespace.getLogoStorageType());
}

@Override
public Path getCachedFile(FileResource resource) throws IOException {
return switch (resource.getStorageType()) {
Expand Down
9 changes: 9 additions & 0 deletions server/src/main/java/org/eclipse/openvsx/util/NamingUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
package org.eclipse.openvsx.util;

import org.apache.commons.lang3.StringUtils;
import org.apache.tika.mime.MimeType;
import org.eclipse.openvsx.adapter.ExtensionQueryResult;
import org.eclipse.openvsx.entities.Extension;
import org.eclipse.openvsx.entities.ExtensionVersion;
import org.eclipse.openvsx.entities.Namespace;
import org.eclipse.openvsx.json.ExtensionJson;
import org.eclipse.openvsx.search.ExtensionSearch;

Expand Down Expand Up @@ -93,4 +95,11 @@ public static ExtensionId fromExtensionId(String text) {
: null;
}

public static String toLogoName(Namespace namespace, MimeType logoType) {
return "logo-" + namespace.getName() + "-" + System.currentTimeMillis() + logoType.getExtension();
}

public static String changeLogoName(Namespace oldNamespace, Namespace newNamespace) {
return oldNamespace.getLogoName().replace("-" + oldNamespace.getName() + "-", "-" + newNamespace.getName() + "-");
}
}
53 changes: 34 additions & 19 deletions webui/src/pages/user/user-namespace-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/

import React, { ChangeEvent, FunctionComponent, useContext, useEffect, useRef, useState } from 'react';
import React, { ChangeEvent, FunctionComponent, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Box, TextField, Typography, Grid, Button, IconButton, Slider, Stack, Dialog, DialogActions, DialogTitle,
DialogContent, InputAdornment, Select, MenuItem, Paper, SelectChangeEvent } from '@mui/material';
import { CheckCircleOutline } from '@mui/icons-material';
Expand Down Expand Up @@ -101,6 +101,11 @@ export const UserNamespaceDetails: FunctionComponent<UserNamespaceDetailsProps>
const [prevEditorPosition, setPrevEditorPosition] = useState<Position>();
const [linkedInAccountType, setLinkedInAccountType] = useState<string>(LINKED_IN_PERSONAL);

const noChanges = useMemo(() => {
const isFalsy = (x: unknown) => !!x === false;
return _.isEqual(_.omitBy(currentDetails, isFalsy), _.omitBy(newDetails, isFalsy));
}, [currentDetails, newDetails]);

useEffect(() => {
getNamespaceDetails();
return () => abortController.current.abort();
Expand Down Expand Up @@ -194,8 +199,15 @@ export const UserNamespaceDetails: FunctionComponent<UserNamespaceDetailsProps>
throw result;
}

if (logoPreview) {
const logoFile = await (await fetch(logoPreview)).blob();
await context.service.setNamespaceLogo(abortController.current, details.name, logoFile, details.logo as string);
await getNamespaceDetails();
} else {
setCurrentDetails(copy(newDetails));
}

setDetailsUpdated(true);
setCurrentDetails(copy(details));
setBannerNamespaceName(details.displayName || details.name);
} catch (err) {
context.handleError(err);
Expand Down Expand Up @@ -303,21 +315,24 @@ export const UserNamespaceDetails: FunctionComponent<UserNamespaceDetailsProps>
setEditing(false);
};

const handleSaveLogo = () => {
const canvasScaled = editor.current?.getImageScaledToCanvas();
if (canvasScaled) {
canvasScaled.toBlob(async (blob) => {
if (blob) {
if (logoPreview) {
URL.revokeObjectURL(logoPreview);
}
setLogoPreview(URL.createObjectURL(blob));
await context.service.setNamespaceLogo(abortController.current, props.namespace.name, blob, dropzoneFile!.name);
await getNamespaceDetails();
const handleApplyLogo = () => {
const avatarEditor = editor.current as AvatarEditor;
const canvasScaled = avatarEditor.getImageScaledToCanvas();
canvasScaled.toBlob(async (blob) => {
if (blob) {
if (logoPreview) {
URL.revokeObjectURL(logoPreview);
}
});
setEditing(false);
}
setLogoPreview(URL.createObjectURL(blob));

if (newDetails) {
const details = copy(newDetails);
details.logo = dropzoneFile!.name;
setNewDetails(details);
}
}
});
setEditing(false);
};

const adjustScale = (x: number) => {
Expand Down Expand Up @@ -420,8 +435,8 @@ export const UserNamespaceDetails: FunctionComponent<UserNamespaceDetailsProps>
</Button>
<Button
autoFocus
onClick={handleSaveLogo} >
Save logo
onClick={handleApplyLogo} >
Apply logo
</Button>
</DialogActions>
</Dialog>
Expand Down Expand Up @@ -603,7 +618,7 @@ export const UserNamespaceDetails: FunctionComponent<UserNamespaceDetailsProps>
</Grid>
</Grid>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button sx={{ ml: { xs: 2, sm: 2, md: 2, lg: 0, xl: 0 } }} variant='outlined' disabled={_.isEqual(currentDetails, newDetails)} onClick={setNamespaceDetails}>
<Button sx={{ ml: { xs: 2, sm: 2, md: 2, lg: 0, xl: 0 } }} variant='outlined' disabled={noChanges} onClick={setNamespaceDetails}>
Save Namespace Details
</Button>
</Grid>
Expand Down
Loading