chore: rename thumbnail parameter from width to size for clarity (#6533)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.19.x

#### What this PR does / why we need it:
重命名缩略图大小的参数名以便和主题端 finder 用法保持一致
同时确保通过 encode 或者没有 encode 的 uri 都可以获取到缩略图

#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/6536/head
guqing 2024-08-27 18:19:19 +08:00 committed by GitHub
parent ac0700e668
commit a5c6d6672f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 76 additions and 91 deletions

View File

@ -5,12 +5,9 @@ import static run.halo.app.infra.FileCategoryMatcher.IMAGE;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import lombok.experimental.UtilityClass;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import run.halo.app.core.extension.attachment.Attachment;
@UtilityClass
@ -41,16 +38,4 @@ public class AttachmentUtils {
throw new IllegalArgumentException(e);
}
}
/**
* Encode uri string to URI.
* This method will decode the uri string first and then encode it.
*/
public static URI encodeUri(String uriStr) {
var decodedUriStr = UriUtils.decode(uriStr, StandardCharsets.UTF_8);
return UriComponentsBuilder.fromUriString(decodedUriStr)
.encode(StandardCharsets.UTF_8)
.build()
.toUri();
}
}

View File

@ -19,7 +19,7 @@ public interface LocalThumbnailService {
* @return The original image URI, {@link NotFoundException} will be thrown if the thumbnail
* record does not exist by the given thumbnail URI
*/
Mono<URI> getOriginalImageUri(String thumbnailUri);
Mono<URI> getOriginalImageUri(URI thumbnailUri);
/**
* <p>Gets thumbnail file resource for the given year, size and filename.</p>
@ -29,7 +29,7 @@ public interface LocalThumbnailService {
* @param thumbnailUri The thumbnail URI string
* @return The thumbnail file resource
*/
Mono<Resource> getThumbnail(String thumbnailUri);
Mono<Resource> getThumbnail(URI thumbnailUri);
/**
* <p>Gets thumbnail file resource for the given URI and size.</p>
@ -77,5 +77,5 @@ public interface LocalThumbnailService {
Path toFilePath(String thumbRelativeUnixPath);
String buildThumbnailUri(String year, ThumbnailSize size, String filename);
URI buildThumbnailUri(String year, ThumbnailSize size, String filename);
}

View File

@ -4,6 +4,8 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
@ -138,8 +140,15 @@ public class ThumbnailGenerator {
static class ImageDownloader {
public Path downloadFile(URL url) throws IOException {
var encodedUri = AttachmentUtils.encodeUri(url.toString());
return downloadFileInternal(encodedUri.toURL());
return downloadFileInternal(encodedUrl(url));
}
private static URL encodedUrl(URL url) {
try {
return new URL(url.toURI().toASCIIString());
} catch (MalformedURLException | URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
Path downloadFileInternal(URL url) throws IOException {

View File

@ -1,6 +1,7 @@
package run.halo.app.core.attachment;
import java.math.BigInteger;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -34,4 +35,8 @@ public class ThumbnailSigner {
throw new RuntimeException(ALGORITHM + " algorithm not found", e);
}
}
public static String generateSignature(URI uri) {
return generateSignature(uri.toASCIIString());
}
}

View File

@ -74,13 +74,13 @@ public class LocalThumbnailServiceImpl implements LocalThumbnailService {
}
@Override
public Mono<URI> getOriginalImageUri(String thumbnailUri) {
public Mono<URI> getOriginalImageUri(URI thumbnailUri) {
return fetchThumbnail(thumbnailUri)
.map(local -> URI.create(local.getSpec().getImageUri()));
}
@Override
public Mono<Resource> getThumbnail(String thumbnailUri) {
public Mono<Resource> getThumbnail(URI thumbnailUri) {
Assert.notNull(thumbnailUri, "Thumbnail URI must not be null.");
return fetchThumbnail(thumbnailUri)
.flatMap(thumbnail -> {
@ -100,7 +100,7 @@ public class LocalThumbnailServiceImpl implements LocalThumbnailService {
.flatMap(this::generate);
}
private Mono<LocalThumbnail> fetchThumbnail(String thumbnailUri) {
private Mono<LocalThumbnail> fetchThumbnail(URI thumbnailUri) {
Assert.notNull(thumbnailUri, "Thumbnail URI must not be null.");
var thumbSignature = ThumbnailSigner.generateSignature(thumbnailUri);
return client.listBy(LocalThumbnail.class, ListOptions.builder()
@ -171,10 +171,10 @@ public class LocalThumbnailServiceImpl implements LocalThumbnailService {
thumbnail.setSpec(new LocalThumbnail.Spec()
.setImageSignature(signatureForImageUri(imageUri))
.setFilePath(toRelativeUnixPath(filePath))
.setImageUri(ensureInSiteUriIsRelative(imageUri).toString())
.setImageUri(ensureInSiteUriIsRelative(imageUri).toASCIIString())
.setSize(size)
.setThumbSignature(thumbSignature)
.setThumbnailUri(thumbnailUri));
.setThumbnailUri(thumbnailUri.toASCIIString()));
return client.create(thumbnail);
});
}
@ -236,8 +236,9 @@ public class LocalThumbnailServiceImpl implements LocalThumbnailService {
}
@Override
public String buildThumbnailUri(String year, ThumbnailSize size, String filename) {
return "/upload/thumbnails/%s/w%s/%s".formatted(year, size.getWidth(), filename);
public URI buildThumbnailUri(String year, ThumbnailSize size, String filename) {
return URI.create("/upload/thumbnails/%s/w%s/%s".formatted(year, size.getWidth(),
filename));
}
private String toRelativeUnixPath(Path filePath) {
@ -266,7 +267,7 @@ public class LocalThumbnailServiceImpl implements LocalThumbnailService {
* image URL to the external URL.</p>
*/
String signatureForImageUri(URI imageUri) {
var uriToSign = ensureInSiteUriIsRelative(imageUri).toString();
var uriToSign = ensureInSiteUriIsRelative(imageUri);
return ThumbnailSigner.generateSignature(uriToSign);
}

View File

@ -48,7 +48,7 @@ public class ThumbnailServiceImpl implements ThumbnailService {
var imageUrl = imageUrlOpt.get();
return fetchThumbnail(imageUri, size)
.map(thumbnail -> URI.create(thumbnail.getSpec().getThumbnailUri()))
.switchIfEmpty(create(imageUrl, size))
.switchIfEmpty(Mono.defer(() -> create(imageUrl, size)))
.onErrorResume(Throwable.class, e -> {
log.warn("Failed to generate thumbnail for image: {}", imageUrl, e);
return Mono.just(URI.create(imageUrl.toString()));
@ -108,8 +108,8 @@ public class ThumbnailServiceImpl implements ThumbnailService {
thumb.getMetadata().setGenerateName("thumb-");
thumb.setSpec(new Thumbnail.Spec()
.setSize(size)
.setThumbnailUri(uri.toString())
.setImageUri(imageUri.toString())
.setThumbnailUri(uri.toASCIIString())
.setImageUri(imageUri.toASCIIString())
.setImageSignature(signatureFor(imageUri))
);
return client.create(thumb)
@ -119,7 +119,7 @@ public class ThumbnailServiceImpl implements ThumbnailService {
private String signatureFor(URI imageUri) {
var uri = localThumbnailService.ensureInSiteUriIsRelative(imageUri);
return ThumbnailSigner.generateSignature(uri.toString());
return ThumbnailSigner.generateSignature(uri);
}
Mono<Thumbnail> fetchThumbnail(URI imageUri, ThumbnailSize size) {

View File

@ -10,7 +10,6 @@ import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.lang.NonNull;
import run.halo.app.core.attachment.AttachmentRootGetter;
import run.halo.app.core.attachment.ThumbnailSigner;
import run.halo.app.core.attachment.ThumbnailSize;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@ -33,10 +32,6 @@ public class LocalThumbnail extends AbstractExtension {
this.status = (status == null ? new Status() : status);
}
public static String signatureFor(String imageUri) {
return ThumbnailSigner.generateSignature(imageUri);
}
@Data
@Accessors(chain = true)
@Schema(name = "LocalThumbnailSpec")

View File

@ -1,7 +1,6 @@
package run.halo.app.theme.endpoint;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.apache.commons.lang3.StringUtils.removeStart;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
@ -27,7 +26,6 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import run.halo.app.core.attachment.AttachmentUtils;
import run.halo.app.core.attachment.LocalThumbnailService;
import run.halo.app.core.attachment.ThumbnailService;
import run.halo.app.core.attachment.ThumbnailSize;
@ -64,7 +62,7 @@ public class ThumbnailEndpoint implements CustomEndpoint {
private Mono<ServerResponse> getThumbnailByUri(ServerRequest request) {
var query = new ThumbnailQuery(request.queryParams());
return thumbnailService.generate(query.getUri(), query.getWidth())
return thumbnailService.generate(query.getUri(), query.getSize())
.defaultIfEmpty(query.getUri())
.flatMap(uri -> ServerResponse.permanentRedirect(uri).build());
}
@ -82,17 +80,20 @@ public class ThumbnailEndpoint implements CustomEndpoint {
if (StringUtils.isBlank(uriStr)) {
throw new ServerWebInputException("Required parameter 'uri' is missing");
}
return AttachmentUtils.encodeUri(uriStr);
try {
return URI.create(uriStr);
} catch (IllegalArgumentException e) {
throw new ServerWebInputException("Invalid URI: " + uriStr);
}
}
@Schema(requiredMode = REQUIRED)
public ThumbnailSize getWidth() {
var width = params.getFirst("width");
if (StringUtils.isBlank(width)) {
throw new ServerWebInputException("Required parameter 'width' is missing");
public ThumbnailSize getSize() {
var size = params.getFirst("size");
if (StringUtils.isBlank(size)) {
throw new ServerWebInputException("Required parameter 'size' is missing");
}
// Remove the 'w' prefix
return ThumbnailSize.fromWidth(removeStart(width, 'w'));
return ThumbnailSize.fromName(size);
}
public static void buildParameters(Builder builder) {
@ -103,8 +104,8 @@ public class ThumbnailEndpoint implements CustomEndpoint {
.required(true))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("width")
.description("The width of the thumbnail")
.name("size")
.description("The size of the thumbnail,available values are s,m,l,xl")
.required(true));
}
}

View File

@ -11,10 +11,10 @@ import reactor.core.publisher.Mono;
public interface ThumbnailFinder {
/**
* Generate thumbnail url from given image url and size.
* Generate thumbnail uri from given image uri and size.
*
* @param size the size of thumbnail to generate
* @return the generated thumbnail url
*/
Mono<String> gen(String url, String size);
Mono<String> gen(String uri, String size);
}

View File

@ -14,9 +14,9 @@ public class ThumbnailFinderImpl implements ThumbnailFinder {
private final ThumbnailService thumbnailService;
@Override
public Mono<String> gen(String url, String size) {
return thumbnailService.generate(URI.create(url), ThumbnailSize.fromName(size))
public Mono<String> gen(String uriStr, String size) {
return thumbnailService.generate(URI.create(uriStr), ThumbnailSize.fromName(size))
.map(URI::toString)
.defaultIfEmpty(url);
.defaultIfEmpty(uriStr);
}
}

View File

@ -1,27 +0,0 @@
package run.halo.app.core.attachment;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link AttachmentUtils}.
*
* @author guqing
* @since 2.19.0
*/
class AttachmentUtilsTest {
@Test
void encodeUriTest() {
String urlStr =
"http://localhost:8090/upload/2022/06/CleanShot 2022-06-12 at 23.30.22@2x.webp";
var result = AttachmentUtils.encodeUri(urlStr);
var targetUriStr =
"http://localhost:8090/upload/2022/06/CleanShot%202022-06-12%20at%2023.30.22@2x.webp";
assertThat(result.toString()).isEqualTo(targetUriStr);
result = AttachmentUtils.encodeUri(targetUriStr);
assertThat(result.toString()).isEqualTo(targetUriStr);
}
}

View File

@ -2,6 +2,7 @@ package run.halo.app.core.attachment;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URI;
import org.junit.jupiter.api.Test;
/**
@ -18,4 +19,21 @@ class ThumbnailSignerTest {
assertThat(signature).isEqualTo(
"ed2e575a1d298e08df19dca9733da3f46eab5b157b1b8f9ed02f31b2f907ec79");
}
}
@Test
void generateSignatureWithUri() {
var uri = URI.create("https://example.com/example.jpg");
var signature = ThumbnailSigner.generateSignature(uri);
assertThat(signature).isEqualTo(
"35b8dc8b5b518222245b3f74ea3c3b292e31eb0d888102b5e5a9cdbe4fb7a976");
uri = URI.create("/upload/%E4%B8%AD%E6%96%87%E5%A4%B4%E5%83%8F%20hello%25avatar.png");
signature = ThumbnailSigner.generateSignature(uri);
var avatarPngHash = "7dc5019aea284094c5ba9c4404e3cedf779a2fec0d836cbdf542da3cb373dd3c";
assertThat(signature).isEqualTo(avatarPngHash);
uri = URI.create("/upload/中文头像%20hello%25avatar.png");
signature = ThumbnailSigner.generateSignature(uri);
assertThat(signature).isEqualTo(avatarPngHash);
}
}

View File

@ -7,6 +7,7 @@ import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static run.halo.app.core.attachment.ThumbnailSigner.generateSignature;
import java.net.MalformedURLException;
import java.net.URI;
@ -54,7 +55,7 @@ class LocalThumbnailServiceImplTest {
void endpointForTest() {
var endpoint =
localThumbnailService.buildThumbnailUri("2024", ThumbnailSize.L, "example.jpg");
assertThat(endpoint).isEqualTo("/upload/thumbnails/2024/w1200/example.jpg");
assertThat(endpoint.toString()).isEqualTo("/upload/thumbnails/2024/w1200/example.jpg");
}
@Test
@ -90,23 +91,20 @@ class LocalThumbnailServiceImplTest {
void signatureForImageUriTest() throws MalformedURLException {
when(externalUrlSupplier.getRaw()).thenReturn(new URL("http://localhost:8090"));
var signature = signatureForImageUriStr("http://localhost:8090/example.jpg");
assertThat(signature).isEqualTo(LocalThumbnail.signatureFor("/example.jpg"));
assertThat(signature).isEqualTo(generateSignature("/example.jpg"));
signature = signatureForImageUriStr("http://localhost:8090/example.jpg");
assertThat(signature).isEqualTo(LocalThumbnail.signatureFor("/example.jpg"));
assertThat(signature).isEqualTo(generateSignature("/example.jpg"));
signature = signatureForImageUriStr("http://localhost:8091/example.jpg");
assertThat(signature).isEqualTo(
LocalThumbnail.signatureFor("http://localhost:8091/example.jpg"));
assertThat(signature).isEqualTo(generateSignature("http://localhost:8091/example.jpg"));
signature = signatureForImageUriStr("localhost:8090/example.jpg");
assertThat(signature).isEqualTo(
LocalThumbnail.signatureFor("localhost:8090/example.jpg"));
assertThat(signature).isEqualTo(generateSignature("localhost:8090/example.jpg"));
when(externalUrlSupplier.getRaw()).thenReturn(null);
signature = signatureForImageUriStr("http://localhost:8090/example.jpg");
assertThat(signature).isEqualTo(
LocalThumbnail.signatureFor("http://localhost:8090/example.jpg"));
assertThat(signature).isEqualTo(generateSignature("http://localhost:8090/example.jpg"));
}
String signatureForImageUriStr(String uriStr) {