diff --git a/application/src/main/java/run/halo/app/core/attachment/ThumbnailMigration.java b/application/src/main/java/run/halo/app/core/attachment/ThumbnailMigration.java deleted file mode 100644 index 5e0372a7c..000000000 --- a/application/src/main/java/run/halo/app/core/attachment/ThumbnailMigration.java +++ /dev/null @@ -1,141 +0,0 @@ -package run.halo.app.core.attachment; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.function.Function; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; -import reactor.util.retry.Retry; -import run.halo.app.core.attachment.extension.LocalThumbnail; -import run.halo.app.core.attachment.extension.Thumbnail; -import run.halo.app.extension.Extension; -import run.halo.app.extension.ListOptions; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.ReactiveExtensionPaginatedOperator; - -/** - *

TODO Remove this class in the next major version.

- * when this class is removed, the following code should be added: - *
- * 
- * schemeManager.register(LocalThumbnail.class, indexSpec -> {
- *       indexSpec.add(new IndexSpec()
- *           // mark the index as unique
- *           .setUnique(true)
- *           .setName(LocalThumbnail.UNIQUE_IMAGE_AND_SIZE_INDEX)
- *           .setIndexFunc(simpleAttribute(LocalThumbnail.class,
- *               LocalThumbnail::uniqueImageAndSize)
- *           )
- *       );
- *       // ...
- *  });
- *  schemeManager.register(Thumbnail.class, indexSpec -> {
- *       indexSpec.add(new IndexSpec()
- *            // mark the index as unique
- *           .setUnique(true)
- *           .setName(Thumbnail.ID_INDEX)
- *           .setIndexFunc(simpleAttribute(Thumbnail.class, Thumbnail::idIndexFunc))
- *       );
- *       // ...
- *  });
- * 
- * 
- * - * @see run.halo.app.infra.SchemeInitializer - * @since 2.20.9 - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class ThumbnailMigration implements ApplicationRunner { - private final LocalThumbnailService localThumbnailService; - private final ReactiveExtensionClient client; - private final ReactiveExtensionPaginatedOperator extensionPaginatedOperator; - - @Override - public void run(ApplicationArguments args) throws Exception { - cleanupThumbnail(Thumbnail.class, - thumbnail -> new UniqueKey(thumbnail.getSpec().getImageUri(), - thumbnail.getSpec().getSize().name())) - .count() - .doOnNext(count -> log.info("Deleted {} duplicate thumbnail records", count)) - .block(); - - cleanupThumbnail(LocalThumbnail.class, - thumbnail -> new UniqueKey(thumbnail.getSpec().getImageUri(), - thumbnail.getSpec().getSize().name())) - .flatMap(thumb -> { - var filePath = localThumbnailService.toFilePath(thumb.getSpec().getFilePath()); - return deleteFile(filePath).thenReturn(thumb.getMetadata().getName()); - }) - .count() - .doOnNext(count -> log.info("Deleted {} duplicate local thumbnail records.", count)) - .block(); - log.info("Duplicate thumbnails have been cleaned up."); - } - - private Mono deleteFile(Path path) { - return Mono.fromRunnable( - () -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - // Ignore - } - }) - .subscribeOn(Schedulers.boundedElastic()) - .then(); - } - - private Flux cleanupThumbnail(Class thumbClass, - Function keyFunction) { - var unique = new HashSet(); - var duplicateThumbs = new ArrayList(); - - var collectDuplicateMono = extensionPaginatedOperator.list(thumbClass, new ListOptions()) - .doOnNext(thumbnail -> { - var key = keyFunction.apply(thumbnail); - if (unique.contains(key)) { - duplicateThumbs.add(thumbnail); - } else { - unique.add(key); - } - }) - .then(); - - return Mono.when(collectDuplicateMono) - .thenMany(Flux.fromIterable(duplicateThumbs) - .flatMap(this::deleteThumbnail) - ); - } - - @SuppressWarnings("unchecked") - private Mono deleteThumbnail(T thumbnail) { - return client.delete(thumbnail) - .onErrorResume(OptimisticLockingFailureException.class, - e -> deleteThumbnail((Class) thumbnail.getClass(), - thumbnail.getMetadata().getName()) - ); - } - - private Mono deleteThumbnail(Class clazz, String name) { - return Mono.defer(() -> client.fetch(clazz, name) - .flatMap(client::delete) - ) - .retryWhen(Retry.backoff(8, Duration.ofMillis(100)) - .filter(OptimisticLockingFailureException.class::isInstance)); - } - - record UniqueKey(String imageUri, String size) { - } -} diff --git a/application/src/main/java/run/halo/app/notification/SubscriptionMigration.java b/application/src/main/java/run/halo/app/notification/SubscriptionMigration.java deleted file mode 100644 index af73ff894..000000000 --- a/application/src/main/java/run/halo/app/notification/SubscriptionMigration.java +++ /dev/null @@ -1,152 +0,0 @@ -package run.halo.app.notification; - -import static run.halo.app.content.NotificationReasonConst.NEW_COMMENT_ON_PAGE; -import static run.halo.app.content.NotificationReasonConst.NEW_COMMENT_ON_POST; -import static run.halo.app.content.NotificationReasonConst.SOMEONE_REPLIED_TO_YOU; -import static run.halo.app.extension.index.query.QueryFactory.and; -import static run.halo.app.extension.index.query.QueryFactory.equal; -import static run.halo.app.extension.index.query.QueryFactory.in; -import static run.halo.app.extension.index.query.QueryFactory.isNull; -import static run.halo.app.extension.index.query.QueryFactory.startsWith; - -import java.util.HashSet; -import java.util.Set; -import java.util.function.Consumer; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.User; -import run.halo.app.core.extension.notification.Subscription; -import run.halo.app.extension.ListOptions; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.router.selector.FieldSelector; -import run.halo.app.infra.AnonymousUserConst; -import run.halo.app.infra.ReactiveExtensionPaginatedOperator; -import run.halo.app.infra.ReactiveExtensionPaginatedOperatorImpl; - -/** - * Subscription migration to adapt to the new expression subscribe mechanism. - * - * @author guqing - * @since 2.15.0 - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class SubscriptionMigration implements ApplicationRunner { - private final NotificationCenter notificationCenter; - private final ReactiveExtensionClient client; - private final SubscriptionService subscriptionService; - private final ReactiveExtensionPaginatedOperator paginatedOperator; - - @Override - public void run(ApplicationArguments args) throws Exception { - handleAnonymousSubscription(); - cleanupUserSubscription(); - } - - private void cleanupUserSubscription() { - var listOptions = new ListOptions(); - var query = isNull("metadata.deletionTimestamp"); - listOptions.setFieldSelector(FieldSelector.of(query)); - var iterator = - new ReactiveExtensionPaginatedOperatorImpl(client); - iterator.list(User.class, listOptions) - .map(user -> user.getMetadata().getName()) - .flatMap(this::removeInternalSubscriptionForUser) - .then() - .doOnSuccess(unused -> log.info("Cleanup user subscription completed")) - .block(); - } - - private void handleAnonymousSubscription() { - log.debug("Start to collating anonymous subscription..."); - Set anonymousSubscribers = new HashSet<>(); - deleteAnonymousSubscription(subscription -> { - var name = subscription.getSpec().getSubscriber().getName(); - anonymousSubscribers.add(name); - }).block(); - if (anonymousSubscribers.isEmpty()) { - return; - } - - // anonymous only subscribe some-one-replied-to-you reason - for (String anonymousSubscriber : anonymousSubscribers) { - createSubscription(anonymousSubscriber, - SOMEONE_REPLIED_TO_YOU, - "props.repliedOwner == '%s'".formatted(anonymousSubscriber)).block(); - } - log.info("Collating anonymous subscription completed."); - } - - private Mono deleteAnonymousSubscription(Consumer consumer) { - var listOptions = new ListOptions(); - var query = and(startsWith("spec.subscriber", AnonymousUserConst.PRINCIPAL), - isNull("spec.reason.expression"), - isNull("metadata.deletionTimestamp"), - in("spec.reason.reasonType", Set.of(NEW_COMMENT_ON_POST, - NEW_COMMENT_ON_PAGE, - SOMEONE_REPLIED_TO_YOU)) - ); - listOptions.setFieldSelector(FieldSelector.of(query)); - return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions) - .doOnNext(consumer) - .doOnNext(subscription -> log.debug("Deleted anonymous subscription: {}", - subscription.getMetadata().getName()) - ) - .then(); - } - - private Mono removeInternalSubscriptionForUser(String username) { - log.debug("Start to collating internal subscription for user: {}", username); - var subscriber = new Subscription.Subscriber(); - subscriber.setName(username); - - var listOptions = new ListOptions(); - var fieldQuery = and(isNull("metadata.deletionTimestamp"), - equal("spec.subscriber", subscriber.toString()), - in("spec.reason.reasonType", Set.of( - NEW_COMMENT_ON_POST, - NEW_COMMENT_ON_PAGE, - SOMEONE_REPLIED_TO_YOU - )) - ); - listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); - - return subscriptionService.removeBy(listOptions) - .map(subscription -> { - var name = subscription.getSpec().getSubscriber().getName(); - var reason = subscription.getSpec().getReason(); - String expression = switch (reason.getReasonType()) { - case NEW_COMMENT_ON_POST -> "props.postOwner == '%s'".formatted(name); - case NEW_COMMENT_ON_PAGE -> "props.pageOwner == '%s'".formatted(name); - case SOMEONE_REPLIED_TO_YOU -> "props.repliedOwner == '%s'".formatted(name); - // never happen - default -> null; - }; - return new SubscriptionSummary(name, reason.getReasonType(), expression); - }) - .distinct() - .flatMap(summary -> createSubscription(summary.subscriber(), summary.reasonType(), - summary.expression())) - .then() - .doOnSuccess(unused -> - log.debug("Collating internal subscription for user: {} completed", username)); - } - - Mono createSubscription(String name, String reasonType, String expression) { - var interestReason = new Subscription.InterestReason(); - interestReason.setReasonType(reasonType); - interestReason.setExpression(expression); - var subscriber = new Subscription.Subscriber(); - subscriber.setName(name); - log.debug("Create subscription for user: {} with reasonType: {}", name, reasonType); - return notificationCenter.subscribe(subscriber, interestReason).then(); - } - - record SubscriptionSummary(String subscriber, String reasonType, String expression) { - } -}