Remove deprecated migrations (#7535)

#### What type of PR is this?

/kind cleanup
/area core
/milestone 2.21.x

#### What this PR does / why we need it:

This PR removes deprecated Subscritpion and Thumbnail migrations.

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/7543/head^2
John Niang 2025-06-13 00:24:40 +08:00 committed by GitHub
parent 2cba28d21d
commit 4a3d35a900
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 0 additions and 293 deletions

View File

@ -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;
/**
* <p>TODO Remove this class in the next major version.</p>
* when this class is removed, the following code should be added:
* <pre>
* <code>
* 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))
* );
* // ...
* });
* </code>
* </pre>
*
* @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<Void> deleteFile(Path path) {
return Mono.fromRunnable(
() -> {
try {
Files.deleteIfExists(path);
} catch (Exception e) {
// Ignore
}
})
.subscribeOn(Schedulers.boundedElastic())
.then();
}
private <T extends Extension> Flux<T> cleanupThumbnail(Class<T> thumbClass,
Function<T, UniqueKey> keyFunction) {
var unique = new HashSet<UniqueKey>();
var duplicateThumbs = new ArrayList<T>();
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 <T extends Extension> Mono<T> deleteThumbnail(T thumbnail) {
return client.delete(thumbnail)
.onErrorResume(OptimisticLockingFailureException.class,
e -> deleteThumbnail((Class<T>) thumbnail.getClass(),
thumbnail.getMetadata().getName())
);
}
private <T extends Extension> Mono<T> deleteThumbnail(Class<T> 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) {
}
}

View File

@ -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<String> 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<Void> deleteAnonymousSubscription(Consumer<Subscription> 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<Void> 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<Void> 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) {
}
}