Skip to content

Commit

Permalink
fix: thumbnail file name conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Aug 13, 2024
1 parent aff1562 commit 2bfdfa6
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,10 @@ public interface LocalThumbnailService {
* @return A {@link Mono} indicates the completion of the deletion.
*/
Mono<Void> delete(URI imageUri);

/**
* Ensures the image URI is an url path if it's an in-site image.
* If it's not an in-site image, it will return directly.
*/
URI ensureInSiteUriIsRelative(URI imageUri);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface ThumbnailService {
* @return generated thumbnail uri if success, otherwise empty.
*/
Mono<URI> generate(URI imageUri, ThumbnailSize size);

Mono<Void> delete(URI imageUri);
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ static String endpointFor(String fileName, String year, ThumbnailSize size) {
return "/upload/thumbnails/%s/w%s/%s".formatted(year, size.getWidth(), fileName);
}

static String getThumbnailFileName(URL imageUrl) {
static String geImageFileName(URL imageUrl) {
var fileName = substringAfterLast(imageUrl.getPath(), "/");
fileName = defaultIfBlank(fileName, randomAlphanumeric(10));
return ThumbnailGenerator.sanitizeFileName(fileName);
Expand Down Expand Up @@ -90,10 +90,7 @@ public Mono<Resource> getThumbnail(String year, ThumbnailSize size, String filen

private Mono<LocalThumbnail> fetchThumbnail(String thumbSignature) {
return client.listBy(LocalThumbnail.class, ListOptions.builder()
.fieldQuery(and(
equal("spec.thumbSignature", thumbSignature),
isNull("metadata.deletionTimestamp"))
)
.fieldQuery(equal("spec.thumbSignature", thumbSignature))
.build(), PageRequestImpl.ofSize(1))
.flatMap(result -> Mono.justOrEmpty(ListResult.first(result)));
}
Expand All @@ -120,16 +117,19 @@ public Mono<LocalThumbnail> create(URL imageUrl, ThumbnailSize size) {
Assert.notNull(imageUrl, "Image URL must not be null.");
Assert.notNull(size, "Thumbnail size must not be null.");
var year = getYear();
return ThumbnailInfo.from(imageUrl, size, attachmentDirGetter.get(), year)
.flatMap(thumbInfo -> {
var originalFileName = geImageFileName(imageUrl);
return generateUniqueThumbFileName(originalFileName, year, size)
.flatMap(thumbFileName -> {
var filePath =
buildThumbnailStorePath(attachmentDirGetter.get(), thumbFileName, year, size);
var thumbnail = new LocalThumbnail();
thumbnail.setMetadata(new Metadata());
thumbnail.getMetadata().setGenerateName("thumbnail-");
var thumbnailUri = endpointFor(thumbInfo.fileName(), year, size);
var thumbnailUri = endpointFor(thumbFileName, year, size);
var thumbSignature = ThumbnailSigner.generateSignature(thumbnailUri);
thumbnail.setSpec(new LocalThumbnail.Spec()
.setImageSignature(signatureForImageUri(imageUrl.toString()))
.setFilePath(toRelativeUnixPath(thumbInfo.filePath()))
.setFilePath(toRelativeUnixPath(filePath))
.setImageUrl(imageUrl.toString())
.setSize(size)
.setThumbSignature(thumbSignature)
Expand All @@ -155,6 +155,35 @@ public Mono<Void> delete(URI imageUri) {
.then();
}

@Override
public URI ensureInSiteUriIsRelative(URI imageUri) {
Assert.notNull(imageUri, "Image URI must not be null.");
var externalUrl = externalUrlSupplier.getRaw();
if (externalUrl == null || !isSameOrigin(imageUri, externalUrl)) {
return imageUri;
}
var uriStr = imageUri.toString().replaceFirst("^\\w+://", "");
uriStr = StringUtils.removeStart(uriStr, imageUri.getAuthority());
return URI.create(uriStr);
}

Mono<String> generateUniqueThumbFileName(String originalFileName, String year,
ThumbnailSize size) {
Assert.notNull(originalFileName, "Original file name must not be null.");
return generateUniqueThumbFileName(originalFileName, originalFileName, year, size);
}

private Mono<String> generateUniqueThumbFileName(String originalFileName, String tryFileName,
String year, ThumbnailSize size) {
var hash = ThumbnailSigner.generateSignature(endpointFor(tryFileName, year, size));
return fetchThumbnail(hash)
.flatMap(thumbnail -> {
// use the original file name to generate a new file name
var newTryFileName = appendRandomSuffix(originalFileName);
return generateUniqueThumbFileName(originalFileName, newTryFileName, year, size);
})
.switchIfEmpty(Mono.just(tryFileName));
}

Path toFilePath(String relativeUnixPath) {
Assert.notNull(relativeUnixPath, "Relative path must not be null.");
Expand Down Expand Up @@ -189,44 +218,20 @@ private Mono<Void> deleteFile(Path path) {
* image URL to the external URL.</p>
*/
String signatureForImageUri(String imageUriStr) {
var externalUrl = externalUrlSupplier.getRaw();
var imageUri = URI.create(imageUriStr);
if (externalUrl == null || !isSameOrigin(imageUri, externalUrl)) {
return ThumbnailSigner.generateSignature(imageUriStr);
}
return ThumbnailSigner.generateSignature(imageUri.getPath());
var uriToSign = ensureInSiteUriIsRelative(imageUri).toString();
return ThumbnailSigner.generateSignature(uriToSign);
}

private boolean isSameOrigin(URI imageUri, URL externalUrl) {
return StringUtils.equals(imageUri.getHost(), externalUrl.getHost())
&& imageUri.getPort() == externalUrl.getPort();
}

record ThumbnailInfo(String fileName, Path filePath) {
public static Mono<ThumbnailInfo> from(URL image, ThumbnailSize size, Path rootPath,
String year) {
return Mono.defer(() -> {
var fileName = getThumbnailFileName(image);
var filePath = generateUniqueFilePath(rootPath, fileName, year, size);
return Mono.just(new ThumbnailInfo(fileName, filePath));
});
}

private static Path generateUniqueFilePath(Path rootPath, String fileName, String year,
ThumbnailSize size) {
var filePath = buildThumbnailStorePath(rootPath, fileName, year, size);
if (Files.exists(filePath)) {
fileName = appendRandomSuffix(fileName);
return generateUniqueFilePath(rootPath, fileName, year, size);
}
return filePath;
}

private static String appendRandomSuffix(String fileName) {
var baseName = StringUtils.substringBeforeLast(fileName, ".");
var extension = substringAfterLast(fileName, ".");
var randomSuffix = randomAlphanumeric(5);
return String.format("%s_%s.%s", baseName, randomSuffix, extension);
}
static String appendRandomSuffix(String fileName) {
var baseName = StringUtils.substringBeforeLast(fileName, ".");
var extension = substringAfterLast(fileName, ".");
var randomSuffix = randomAlphanumeric(6);
return String.format("%s_%s.%s", baseName, randomSuffix, extension);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package run.halo.app.core.attachment.impl;

import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.startsWith;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
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.ThumbnailProvider;
import run.halo.app.core.attachment.ThumbnailProvider.ThumbnailContext;
import run.halo.app.core.attachment.ThumbnailService;
import run.halo.app.core.attachment.ThumbnailSigner;
import run.halo.app.core.attachment.ThumbnailSize;
import run.halo.app.core.extension.attachment.Thumbnail;
import run.halo.app.extension.ListOptions;
Expand All @@ -30,6 +36,8 @@ public class ThumbnailServiceImpl implements ThumbnailService {
private final ExtensionGetter extensionGetter;
private final ReactiveExtensionClient client;
private final ExternalLinkProcessor externalLinkProcessor;
private final ThumbnailProvider thumbnailProvider;
private final LocalThumbnailService localThumbnailService;

@Override
public Mono<URI> generate(URI imageUri, ThumbnailSize size) {
Expand All @@ -47,6 +55,33 @@ public Mono<URI> generate(URI imageUri, ThumbnailSize size) {
});
}

@Override
public Mono<Void> delete(URI imageUri) {
Assert.notNull(imageUri, "Image uri must not be null");
Mono<Void> deleteMono;
if (imageUri.isAbsolute()) {
deleteMono = thumbnailProvider.delete(AttachmentUtils.toUrl(imageUri));
} else {
// Local thumbnails maybe a relative path, so we need to process it.
deleteMono = localThumbnailService.delete(imageUri);
}
return deleteMono.then(deleteThumbnailRecord(imageUri));
}

private Mono<Void> deleteThumbnailRecord(URI imageUri) {
var imageUrlOpt = toImageUrl(imageUri);
if (imageUrlOpt.isEmpty()) {
return Mono.empty();
}
var imageHash = signatureFor(imageUri.toString());
var listOptions = ListOptions.builder()
.fieldQuery(startsWith(Thumbnail.ID_INDEX, Thumbnail.idIndexFunc(imageHash, "")))
.build();
return client.listAll(Thumbnail.class, listOptions, Sort.unsorted())
.flatMap(client::delete)
.then();
}

Optional<URL> toImageUrl(URI imageUri) {
try {
if (imageUri.isAbsolute()) {
Expand Down Expand Up @@ -77,15 +112,20 @@ Mono<URI> create(URL imageUrl, ThumbnailSize size) {
.setSize(size)
.setThumbnailUri(uri.toString())
.setImageUrl(imageUrl)
.setImageSignature(Thumbnail.signatureFor(imageUrl.toString()))
.setImageSignature(signatureFor(imageUrl.toString()))
);
return client.create(thumb)
.thenReturn(uri);
});
}

private Mono<Thumbnail> fetchThumbnail(URL imageUrl, ThumbnailSize size) {
var imageHash = Thumbnail.signatureFor(imageUrl.toString());
private String signatureFor(String imageUri) {
var uri = localThumbnailService.ensureInSiteUriIsRelative(URI.create(imageUri));
return ThumbnailSigner.generateSignature(uri.toString());
}

Mono<Thumbnail> fetchThumbnail(URL imageUrl, ThumbnailSize size) {
var imageHash = signatureFor(imageUrl.toString());
var id = Thumbnail.idIndexFunc(imageHash, size.name());
return client.listBy(Thumbnail.class, ListOptions.builder()
.fieldQuery(equal(Thumbnail.ID_INDEX, id))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.net.URI;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -48,7 +49,9 @@ public Result reconcile(Request request) {
return;
}
// add finalizer
addFinalizers(attachment.getMetadata(), Set.of(Constant.FINALIZER_NAME));
if (addFinalizers(attachment.getMetadata(), Set.of(Constant.FINALIZER_NAME))) {
client.update(attachment);
}

var annotations = attachment.getMetadata().getAnnotations();
if (annotations != null) {
Expand Down Expand Up @@ -114,6 +117,11 @@ void updateStatus(String attachmentName, AttachmentStatus status) {
}

void cleanUpResources(Attachment attachment) {
Optional.ofNullable(attachment.getStatus())
.map(AttachmentStatus::getPermalink)
.map(URI::create)
.ifPresent(uri -> thumbnailService.delete(uri).block());

attachmentService.delete(attachment).block();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
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;
Expand Down Expand Up @@ -48,8 +47,4 @@ public static String idIndexFunc(Thumbnail thumbnail) {
public static String idIndexFunc(String imageHash, String size) {
return imageHash + "-" + size;
}

public static String signatureFor(String imageUri) {
return ThumbnailSigner.generateSignature(imageUri);
}
}
Loading

0 comments on commit 2bfdfa6

Please sign in to comment.