Skip to content

Commit

Permalink
refactor: snapshot attributes and post publish (#2709)
Browse files Browse the repository at this point in the history
#### What type of PR is this?
/kind improvement
/area core
/milestone 2.0
 
#### What this PR does / why we need it:
1. 简化 Snapshot 的指责,去除 Snapshot 中关于 version,publishTime 等与发布相关的东西,Snapshot本身没有发布概念
2. 对应修改文章和自定义页面,将版本的概念转移到拥有 Snapshot 的模型上,如 Post 和 SinglePage

⚠️此 PR 为破坏性更新,对旧数据不兼容,可能导致旧数据文章内容无法显示问题

Console 端需要做出一些修改:
- 将文章发布前调用保存内容的 API 修改为 `/posts/{name}/content`
- 将自定义页面发布前调用保存内容的 API 修改为 `/singlepages/{name}/content`

发布接口提供了一个 `async` 参数,默认值为 `false`此时发布API需要等待发布成功后才返回结果,如果等待超时则提示`Publishing wait timeout.`, 如果传递 `async=true` 表示异步发布,更新完数据就成功,reconciler 在后台慢慢执行。

#### Special notes for your reviewer:
how to test it?
1. 新创建一篇文章,测试点击保存是否成功
2. 新创建一篇文章,测试编辑内容后直接发布是否成功
3. 测试发布过的文章的取消发布状态是否显示未发布状态
4. 对于 Snapshot,新建文章点保存会创建一个snapshot,点击发布则是更新之前点保存创建的一条,所以记录数不会多,发布之后进入编辑随便写点内容直接点发布后查询snapshot则会多一条记录。
5. 文章的 status 里的 conditions 最新添加的都在第一个即头插入,如果连续两次发布则不会在增加 condition,长度超过20条会把旧的 condition 删除掉
6. 自定义页面上同

/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
Action Required: 简化 Snapshot 模型的职责,去除版本相关属性
```
  • Loading branch information
guqing authored Nov 18, 2022
1 parent e48c228 commit cca95cb
Show file tree
Hide file tree
Showing 32 changed files with 1,108 additions and 923 deletions.
9 changes: 4 additions & 5 deletions src/main/java/run/halo/app/config/ExtensionConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.content.ContentService;
import run.halo.app.content.PostService;
import run.halo.app.content.SinglePageService;
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
import run.halo.app.content.permalinks.PostPermalinkPolicy;
import run.halo.app.content.permalinks.TagPermalinkPolicy;
Expand Down Expand Up @@ -151,11 +150,11 @@ Controller themeController(ExtensionClient client, HaloProperties haloProperties
@Bean
Controller postController(ExtensionClient client, ContentService contentService,
PostPermalinkPolicy postPermalinkPolicy, CounterService counterService,
PostService postService) {
PostService postService, ApplicationContext applicationContext) {
return new ControllerBuilder("post", client)
.reconciler(new PostReconciler(client, contentService, postService,
postPermalinkPolicy,
counterService))
counterService, applicationContext))
.extension(new Post())
// TODO Make it configurable
.workerCount(10)
Expand Down Expand Up @@ -204,10 +203,10 @@ Controller attachmentController(ExtensionClient client,
@Bean
Controller singlePageController(ExtensionClient client, ContentService contentService,
ApplicationContext applicationContext, CounterService counterService,
SinglePageService singlePageService, ExternalUrlSupplier externalUrlSupplier) {
ExternalUrlSupplier externalUrlSupplier) {
return new ControllerBuilder("single-page", client)
.reconciler(new SinglePageReconciler(client, contentService,
applicationContext, singlePageService, counterService, externalUrlSupplier)
applicationContext, counterService, externalUrlSupplier)
)
.extension(new SinglePage())
.build();
Expand Down
15 changes: 4 additions & 11 deletions src/main/java/run/halo/app/content/ContentRequest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package run.halo.app.content;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.HashMap;
import org.apache.commons.lang3.StringUtils;
import run.halo.app.core.extension.Snapshot;
import run.halo.app.extension.Metadata;
Expand All @@ -17,31 +18,23 @@ public record ContentRequest(@Schema(required = true) Ref subjectRef,
@Schema(required = true) String rawType) {

public Snapshot toSnapshot() {
Snapshot snapshot = new Snapshot();
final Snapshot snapshot = new Snapshot();

Metadata metadata = new Metadata();
metadata.setName(defaultName(subjectRef));
metadata.setAnnotations(new HashMap<>());
snapshot.setMetadata(metadata);

Snapshot.SnapShotSpec snapShotSpec = new Snapshot.SnapShotSpec();
snapShotSpec.setSubjectRef(subjectRef);
snapShotSpec.setVersion(1);

snapShotSpec.setRawType(rawType);
snapShotSpec.setRawPatch(StringUtils.defaultString(raw()));
snapShotSpec.setContentPatch(StringUtils.defaultString(content()));
String displayVersion = Snapshot.displayVersionFrom(snapShotSpec.getVersion());
snapShotSpec.setDisplayVersion(displayVersion);

snapshot.setSpec(snapShotSpec);
return snapshot;
}

private String defaultName(Ref subjectRef) {
// example: Post-apost-v1-snapshot
return String.join("-", subjectRef.getKind(),
subjectRef.getName(), "v1", "snapshot");
}

public String rawPatchFrom(String originalRaw) {
// originalRaw content from v1
return PatchUtils.diffToJsonPatch(originalRaw, this.raw);
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/run/halo/app/content/ContentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ public interface ContentService {

Mono<ContentWrapper> draftContent(ContentRequest content);

Mono<ContentWrapper> updateContent(ContentRequest content);
Mono<ContentWrapper> draftContent(ContentRequest content, String parentName);

Mono<ContentWrapper> publish(String headSnapshotName, Ref subjectRef);
Mono<ContentWrapper> updateContent(ContentRequest content);

Mono<Snapshot> getBaseSnapshot(Ref subjectRef);

Mono<Snapshot> latestSnapshotVersion(Ref subjectRef);

Mono<Snapshot> latestPublishedSnapshot(Ref subjectRef);

Flux<Snapshot> listSnapshots(Ref subjectRef);
}
1 change: 0 additions & 1 deletion src/main/java/run/halo/app/content/ContentWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
@Builder
public class ContentWrapper {
private String snapshotName;
private Integer version;
private String raw;
private String content;
private String rawType;
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/run/halo/app/content/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ public interface PostService {
Mono<Post> draftPost(PostRequest postRequest);

Mono<Post> updatePost(PostRequest postRequest);

Mono<Post> publishPost(String postName);
}
2 changes: 0 additions & 2 deletions src/main/java/run/halo/app/content/SinglePageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ public interface SinglePageService {
Mono<SinglePage> draft(SinglePageRequest pageRequest);

Mono<SinglePage> update(SinglePageRequest pageRequest);

Mono<SinglePage> publish(String name);
}
211 changes: 66 additions & 145 deletions src/main/java/run/halo/app/content/impl/ContentServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import java.security.Principal;
import java.time.Instant;
import java.util.Comparator;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import java.util.function.Function;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.thymeleaf.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.ContentRequest;
Expand All @@ -28,10 +28,6 @@
*/
@Component
public class ContentServiceImpl implements ContentService {
private static final Comparator<Snapshot> SNAPSHOT_COMPARATOR =
Comparator.comparing(snapshot -> snapshot.getSpec().getVersion());
public static Comparator<Snapshot> LATEST_SNAPSHOT_COMPARATOR = SNAPSHOT_COMPARATOR.reversed();

private final ReactiveExtensionClient client;

public ContentServiceImpl(ReactiveExtensionClient client) {
Expand All @@ -46,61 +42,56 @@ public Mono<ContentWrapper> getContent(String name) {
}

@Override
public Mono<ContentWrapper> draftContent(ContentRequest contentRequest) {
return getContextUsername()
.flatMap(username -> {
// create snapshot
Snapshot snapshot = contentRequest.toSnapshot();
snapshot.addContributor(username);
return client.create(snapshot)
.flatMap(this::restoredContent);
});
public Mono<ContentWrapper> draftContent(ContentRequest content) {
return this.draftContent(content, content.headSnapshotName());
}

@Override
public Mono<ContentWrapper> draftContent(ContentRequest contentRequest, String parentName) {
return Mono.defer(
() -> {
Snapshot snapshot = contentRequest.toSnapshot();
snapshot.getMetadata().setName(UUID.randomUUID().toString());
snapshot.getSpec().setParentSnapshotName(parentName);
return getBaseSnapshot(contentRequest.subjectRef())
.defaultIfEmpty(snapshot)
.map(baseSnapshot -> determineRawAndContentPatch(snapshot, baseSnapshot,
contentRequest))
.flatMap(source -> getContextUsername()
.map(username -> {
Snapshot.addContributor(source, username);
source.getSpec().setOwner(username);
return source;
})
.defaultIfEmpty(source)
);
})
.flatMap(snapshot -> client.create(snapshot)
.flatMap(this::restoredContent));
}

@Override
public Mono<ContentWrapper> updateContent(ContentRequest contentRequest) {
Assert.notNull(contentRequest, "The contentRequest must not be null");
Assert.notNull(contentRequest.headSnapshotName(), "The headSnapshotName must not be null");
return Mono.zip(getContextUsername(),
client.fetch(Snapshot.class, contentRequest.headSnapshotName()))
.flatMap(tuple -> {
String username = tuple.getT1();
Snapshot headSnapShot = tuple.getT2();
return handleSnapshot(headSnapShot, contentRequest, username);
})
Ref subjectRef = contentRequest.subjectRef();
return client.fetch(Snapshot.class, contentRequest.headSnapshotName())
.flatMap(headSnapshot -> getBaseSnapshot(subjectRef)
.map(baseSnapshot -> determineRawAndContentPatch(headSnapshot, baseSnapshot,
contentRequest)
)
)
.flatMap(headSnapshot -> getContextUsername()
.map(username -> {
Snapshot.addContributor(headSnapshot, username);
return headSnapshot;
})
.defaultIfEmpty(headSnapshot)
)
.flatMap(client::update)
.flatMap(this::restoredContent);
}

@Override
public Mono<ContentWrapper> publish(String headSnapshotName, Ref subjectRef) {
Assert.notNull(headSnapshotName, "The headSnapshotName must not be null");
return client.fetch(Snapshot.class, headSnapshotName)
.flatMap(snapshot -> {
if (snapshot.isPublished()) {
// there is nothing to publish
return restoredContent(snapshot.getMetadata().getName(),
subjectRef);
}
Map<String, String> labels = ExtensionUtil.nullSafeLabels(snapshot);
Snapshot.putPublishedLabel(labels);
Snapshot.SnapShotSpec snapshotSpec = snapshot.getSpec();
snapshotSpec.setPublishTime(Instant.now());
snapshotSpec.setDisplayVersion(
Snapshot.displayVersionFrom(snapshotSpec.getVersion()));
return client.update(snapshot)
.then(Mono.defer(
() -> restoredContent(snapshot.getMetadata().getName(), subjectRef))
);
});
}

private Mono<ContentWrapper> restoredContent(String snapshotName,
Ref subjectRef) {
return getBaseSnapshot(subjectRef)
.flatMap(baseSnapshot -> client.fetch(Snapshot.class, snapshotName)
.map(snapshot -> snapshot.applyPatch(baseSnapshot)));
}

private Mono<ContentWrapper> restoredContent(Snapshot headSnapshot) {
return getBaseSnapshot(headSnapshot.getSpec().getSubjectRef())
.map(headSnapshot::applyPatch);
Expand All @@ -109,78 +100,17 @@ private Mono<ContentWrapper> restoredContent(Snapshot headSnapshot) {
@Override
public Mono<Snapshot> getBaseSnapshot(Ref subjectRef) {
return listSnapshots(subjectRef)
.filter(snapshot -> snapshot.getSpec().getVersion() == 1)
.sort(createTimeReversedComparator().reversed())
.filter(p -> StringUtils.equals(Boolean.TRUE.toString(),
ExtensionUtil.nullSafeAnnotations(p).get(Snapshot.KEEP_RAW_ANNO)))
.next();
}

private Mono<Snapshot> handleSnapshot(Snapshot headSnapshot, ContentRequest contentRequest,
String username) {
Ref subjectRef = contentRequest.subjectRef();
return getBaseSnapshot(subjectRef).flatMap(baseSnapshot -> {
String baseSnapshotName = baseSnapshot.getMetadata().getName();
return latestPublishedSnapshot(subjectRef)
.flatMap(latestReleasedSnapshot -> {
Snapshot newSnapshot = contentRequest.toSnapshot();
newSnapshot.getSpec().setSubjectRef(subjectRef);
newSnapshot.addContributor(username);
// has released snapshot, there are 3 assumptions:
// if headPtr != releasePtr && head is not published, then update its content
// directly
// if headPtr != releasePtr && head is published, then create a new snapshot
// if headPtr == releasePtr, then create a new snapshot too
return latestSnapshotVersion(subjectRef)
.flatMap(latestSnapshot -> {
String headSnapshotName = contentRequest.headSnapshotName();
newSnapshot.getSpec()
.setVersion(latestSnapshot.getSpec().getVersion() + 1);
newSnapshot.getSpec().setDisplayVersion(
Snapshot.displayVersionFrom(newSnapshot.getSpec().getVersion()));
newSnapshot.getSpec()
.setParentSnapshotName(headSnapshotName);
// head is published or headPtr == releasePtr
String releasedSnapshotName =
latestReleasedSnapshot.getMetadata().getName();
if (headSnapshot.isPublished() || StringUtils.equals(headSnapshotName,
releasedSnapshotName)) {
String latestSnapshotName = latestSnapshot.getMetadata().getName();
if (!headSnapshotName.equals(latestSnapshotName)
&& !latestSnapshot.isPublished()) {
// publish it then create new one
return publish(latestSnapshotName, subjectRef)
.then(createNewSnapshot(newSnapshot, baseSnapshotName,
contentRequest));
}
// create a new snapshot,done
return createNewSnapshot(newSnapshot, baseSnapshotName,
contentRequest);
}

// otherwise update its content directly
return updateRawAndContentToHeadSnapshot(headSnapshot, baseSnapshotName,
contentRequest);
});
})
// no released snapshot, indicating v1 now, just update the content directly
.switchIfEmpty(Mono.defer(
() -> updateRawAndContentToHeadSnapshot(headSnapshot, baseSnapshotName,
contentRequest)));
});
}

@Override
public Mono<Snapshot> latestSnapshotVersion(Ref subjectRef) {
Assert.notNull(subjectRef, "The subjectRef must not be null.");
return listSnapshots(subjectRef)
.sort(LATEST_SNAPSHOT_COMPARATOR)
.next();
}

@Override
public Mono<Snapshot> latestPublishedSnapshot(Ref subjectRef) {
Assert.notNull(subjectRef, "The subjectRef must not be null.");
return listSnapshots(subjectRef)
.filter(Snapshot::isPublished)
.sort(LATEST_SNAPSHOT_COMPARATOR)
.sort(createTimeReversedComparator())
.next();
}

Expand All @@ -197,47 +127,38 @@ private Mono<String> getContextUsername() {
.map(Principal::getName);
}

private Mono<Snapshot> updateRawAndContentToHeadSnapshot(Snapshot snapshotToUpdate,
String baseSnapshotName,
ContentRequest contentRequest) {
return client.fetch(Snapshot.class, baseSnapshotName)
.flatMap(baseSnapshot -> {
determineRawAndContentPatch(snapshotToUpdate,
baseSnapshot, contentRequest);
return client.update(snapshotToUpdate)
.thenReturn(snapshotToUpdate);
});
}

private Mono<Snapshot> createNewSnapshot(Snapshot snapshotToCreate,
String baseSnapshotName,
ContentRequest contentRequest) {
return client.fetch(Snapshot.class, baseSnapshotName)
.flatMap(baseSnapshot -> {
determineRawAndContentPatch(snapshotToCreate,
baseSnapshot, contentRequest);
snapshotToCreate.getMetadata().setName(UUID.randomUUID().toString());
snapshotToCreate.getSpec().setSubjectRef(contentRequest.subjectRef());
return client.create(snapshotToCreate)
.thenReturn(snapshotToCreate);
});
}

private void determineRawAndContentPatch(Snapshot snapshotToUse, Snapshot baseSnapshot,
private Snapshot determineRawAndContentPatch(Snapshot snapshotToUse, Snapshot baseSnapshot,
ContentRequest contentRequest) {
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
Assert.notNull(contentRequest, "The contentRequest must not be null.");
Assert.notNull(snapshotToUse, "The snapshotToUse not be null.");
String originalRaw = baseSnapshot.getSpec().getRawPatch();
String originalContent = baseSnapshot.getSpec().getContentPatch();
String baseSnapshotName = baseSnapshot.getMetadata().getName();

snapshotToUse.getSpec().setLastModifyTime(Instant.now());
// it is the v1 snapshot, set the content directly
if (snapshotToUse.getSpec().getVersion() == 1) {
if (StringUtils.equals(baseSnapshotName, snapshotToUse.getMetadata().getName())) {
snapshotToUse.getSpec().setRawPatch(contentRequest.raw());
snapshotToUse.getSpec().setContentPatch(contentRequest.content());
ExtensionUtil.nullSafeAnnotations(snapshotToUse)
.put(Snapshot.KEEP_RAW_ANNO, Boolean.TRUE.toString());
} else {
// otherwise diff a patch based on the v1 snapshot
String revisedRaw = contentRequest.rawPatchFrom(originalRaw);
String revisedContent = contentRequest.contentPatchFrom(originalContent);
snapshotToUse.getSpec().setRawPatch(revisedRaw);
snapshotToUse.getSpec().setContentPatch(revisedContent);
}
return snapshotToUse;
}

Comparator<Snapshot> createTimeReversedComparator() {
Function<Snapshot, String> name = snapshot -> snapshot.getMetadata().getName();
Function<Snapshot, Instant> createTime = snapshot -> snapshot.getMetadata()
.getCreationTimestamp();
return Comparator.comparing(createTime)
.thenComparing(name)
.reversed();
}
}
Loading

0 comments on commit cca95cb

Please sign in to comment.