diff --git a/CHANGELOG.md b/CHANGELOG.md index c151476d19..77a01fcc8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # CHANGELOG +# 1.5.3 + +## Improvements + +- 优化邮件发送异常信息处理。 halo-dev/halo#1860 @ntdgy @superdgy +- 优化静态存储的资源映射处理逻辑,支持手动操作 `.halo/static` 目录后,在后台通过刷新按钮更新资源映射。 halo-dev/halo#1907 @Yhcrown @muyunil +- 优化文章字数统计的算法。将中文和其他字符分开统计,中文按照字数计数,其他的语言默认按照标点分割来计数。 halo-dev/halo#1865 @Yhcrown @Tanhex +- 优化后台部分弹窗中表单在移动端的布局。 halo-dev/halo-admin#564 @ruibaby + +## Bug Fixes + +- 修复在 Windows 平台下,因为 H2 Database 文件被占用导致无法全站备份的问题。 halo-dev/halo#1867 @anshangPro +- 修复在 1.5.x 版本中,文章搜索没有关联查询内容(contents)的问题。 halo-dev/halo#1873 @Yhcrown @guqing +- 修复本地上传附件过程中如果发生异常,没有完整打印异常信息栈的问题。 halo-dev/halo#1913 @JohnNiang +- 修复在系统初始化之后,仍然可以通过 `/install` 跳转到登录页面的问题。 halo-dev/halo#1908 @Ljfanny @littlesleep +- 修复评论通知无法正常发送邮件的问题。 halo-dev/halo#1916 @JohnNiang @hapke24 +- 修复后台仪表盘中最近文章的标题过长导致样式异常的问题。 halo-dev/halo-admin#545 @Aanko @hotspring-zwb +- 修复后台带有分页的数据列表中,删除最后一页的所有数据后导致分页页码异常的问题。 halo-dev/halo-admin#550 @QuentinHsu @luohongqu +- 修复后台修复因为缓存数据,重新安装会出现循环进入 install 路由的问题。 halo-dev/halo-admin#558 @ruibaby @Ljfanny + +## Dependencies + +- 更新后台 @halo-dev/editor 版本。 halo-dev/halo-admin#562 @ruibaby + - 修复在改变编辑器布局后导致重复初始化编辑器的问题。 + # 1.5.2 ## Improvements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb827c9c61..9638493f30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,64 +1,107 @@ -> 欢迎你参与 Halo 的开发,下面是参与代码贡献的指南,以供参考。 +# 开源参与指南 -### 代码贡献步骤 +欢迎关注并有想法参与 Halo 的开发,以下是关于如何参与到 Halo 项目的指南,仅供参考。 -#### 0. 提交 issue +## 发现 Issue -任何新功能或者功能改进建议都先提交 issue 讨论一下再进行开发,bug 修复可以直接提交 pull request。 +所有的代码尽可能都有依据(Issue),不是凭空产生。 -#### 1. Fork 此仓库 +### 寻找一个 Good First Issue -点击右上角的 `fork` 按钮即可。 +> 这个步骤非常适合首次贡献者。 -#### 2. Clone 仓库到本地 +在 [halo-dev](https://github.com/halo-dev) 组织下,有非常多的仓库。每个仓库下都有可能包含一些“首次贡献者”友好的 Issue,主要是为了给贡献者提供一个友好的体验。 该类 Issue +一般会用 `good-first-issue` 标签标记。标签 `good-first-issue` 表示该 Issue 不需要对 Halo 有深入的理解也能够参与。 -```bash -git clone https://github.com/{YOUR_USERNAME}/halo +请点击:[good-first-issue](https://github.com/issues?q=org%3Ahalo-dev+is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+no%3Aassignee+) +查看关于 Halo 的 Good First Issue。 -git submodule init +### 认领 Issue -git submodule update -``` +若对任何一个 Issue 感兴趣,请尝试在 Issue 进行回复,讨论解决 Issue 的思路。确定后可直接通过 `/assign` 或者 `/assign @GitHub 用户名` 认领这个 +Issue。这样可避免两位贡献者在同一个问题上花时间。 -#### 3. 创建新的开发分支 +## 代码贡献步骤 -```bash -git checkout -b {BRANCH_NAME} -``` +1. Fork 此仓库 -#### 4. 提交代码 + 点击 Halo 仓库主页右上角的 `Fork` 按钮即可。 -```bash -git push origin {BRANCH_NAME} -``` +2. Clone 仓库到本地 -#### 5. 提交 pull request + ```bash + git clone https://github.com/{YOUR_USERNAME}/halo --recursive + # 或者 git clone git@github.com:{YOUR_USERNAME}/halo.git --recursive + ``` -回到自己的仓库页面,选择 `New pull request` 按钮,创建 `Pull request` 到原仓库的 `master` 分支。 +3. 添加主仓库 -然后等待我们 Review 即可,如有 `Change Request`,再本地修改之后再次提交即可。 + 添加主仓库方便未来同步主仓库最新的 commits 以及创建新的分支。 -#### 6. 更新主仓库代码到自己的仓库 + ```bash + git remote add upstream https://github.com/halo-dev/halo.git + # 或者 git remote add upstream git@github.com:halo-dev/halo.git + git fetch upstream master + ``` -```bash -git remote add upstream git@github.com:halo-dev/halo.git +6. 创建新的开发分支 -git pull upstream master + 我们需要从主仓库的主分支创建一个新的开发分支。 -git push -``` + ```bash + git checkout upstream/master + git checkout -b {BRANCH_NAME} + ``` -### 开发规范 +7. 提交代码 + + ```bash + git add . + git commit -s -m "Fix a bug king" + git push origin {BRANCH_NAME} + ``` -请参考 [https://docs.halo.run/developer-guide/core/code-style](https://docs.halo.run/developer-guide/core/code-style),请确保所有代码格式化之后再提交。 +8. 合并主分支 -### Usage of Cherry Pick Script + 在提交 Pull Request 之前,尽量保证当前分支和主分支的代码尽可能同步,这时需要我们手动操作。示例: -We can use the cherry pick script to cherry-pick commits in pull request as follows: + ```bash + git fetch upstream/master + git merge upstream/master + git push origin {BRANCH_NAME} + ``` + +## Pull Request + +进入此阶段说明已经完成了代码的编写,测试和自测,并且准备好接受 Code Review。 + +### 创建 Pull Request + +回到自己的仓库页面,选择 `New pull request` 按钮,创建 `Pull request` 到原仓库的 `master` 分支。 +然后等待我们 Review 即可,如有 `Change Request`,再本地修改之后再次提交即可。 + +提交 Pull Request 的注意事项: + +- 提交 Pull Request 请充分自测。 +- 每个 Pull Request 尽量只解决一个 Issue,特殊情况除外。 +- 应尽可能多的添加单元测试,其他测试(集成测试和 E2E 测试)可看情况添加。 +- 不论需要解决的 Issue 发生在哪个版本,提交 Pull Request 的时候,请将主仓库的主分支设置为 `master`。例如:即使某个 Bug 于 Halo 1.4.x 被发现,但是提交 Pull Request 仍只针对 + `master` 分支,等待 Pull Request 合并之后,我们会通过 `/cherrypick release-1.4` 或者 `/cherry-pick release-1.4` 指令将此 Pull Request + 的修改应用到 `release-1.4` 和 `release-1.5` 分支上。 + +### 更新 commits + +Code Review 阶段可能需要 Pull Request 作者重新修改代码,请直接在当前分支 commit 并 push 即可,无需关闭并重新提交 Pull Request。示例: ```bash -GITHUB_USER={your_github_user} hack/cherry_pick_pull.sh upstream/{target_branch} {pull_request_number} +git add . +git commit -s -m "Refactor some code according code review" +git push origin bug/king ``` -> This script is from . +同时,若已经进入 Code Review 阶段,请不要强制推送 commits 到当前分支。否则 Reviewers 需要从头开始 Code Review。 + +### 开发规范 +请参考 [https://docs.halo.run/developer-guide/core/code-style](https://docs.halo.run/developer-guide/core/code-style) +,请确保所有代码格式化之后再提交。 diff --git a/README.md b/README.md index 9696f7de5d..46d67164ec 100755 --- a/README.md +++ b/README.md @@ -24,27 +24,7 @@ ## 快速开始 -### Fat Jar - -下载最新的 Halo 运行包: - -```bash -curl -L https://github.com/halo-dev/halo/releases/download/v1.5.2/halo-1.5.2.jar --output halo.jar -``` - -其他地址: - -```bash -java -jar halo.jar -``` - -### Docker - -```bash -docker run -it -d --name halo -p 8090:8090 -v ~/.halo:/root/.halo --restart=always halohub/halo:1.5.2 -``` - -详细部署文档请查阅: +详细部署文档请查阅: ## 在线体验 diff --git a/build.gradle b/build.gradle index 1ed2616255..dfc35d55d9 100644 --- a/build.gradle +++ b/build.gradle @@ -75,6 +75,8 @@ ext { jsoupVersion = '1.14.3' embeddedRedisVersion = '0.6' diffUtilsVersion = '4.11' + githubApiVersion = '1.306' + githubClientVersion = '0.1.32' } dependencies { @@ -126,6 +128,9 @@ dependencies { implementation "com.google.zxing:core:$zxingVersion" implementation "io.github.java-diff-utils:java-diff-utils:$diffUtilsVersion" + implementation "org.kohsuke:github-api:$githubApiVersion" + + implementation "org.iq80.leveldb:leveldb:$levelDbVersion" runtimeOnly "com.h2database:h2:$h2Version" runtimeOnly "mysql:mysql-connector-java" diff --git a/gradle.properties b/gradle.properties index 9eaa2f5c6b..625fdf5a81 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.5.3-SNAPSHOT +version=1.6.0-SNAPSHOT diff --git a/src/main/java/run/halo/app/Application.java b/src/main/java/run/halo/app/Application.java index ba8420eb68..af13934a54 100755 --- a/src/main/java/run/halo/app/Application.java +++ b/src/main/java/run/halo/app/Application.java @@ -1,7 +1,9 @@ package run.halo.app; +import java.util.List; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import run.halo.app.utils.VmUtils; /** * Halo main class. diff --git a/src/main/java/run/halo/app/config/HaloConfiguration.java b/src/main/java/run/halo/app/config/HaloConfiguration.java index def9dfaff8..02bb0c6ed1 100644 --- a/src/main/java/run/halo/app/config/HaloConfiguration.java +++ b/src/main/java/run/halo/app/config/HaloConfiguration.java @@ -1,10 +1,30 @@ package run.halo.app.config; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.util.Timer; +import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; +import okhttp3.Cache; +import okhttp3.OkHttpClient; +import org.jetbrains.annotations.NotNull; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.GitHubRateLimitHandler; +import org.kohsuke.github.HttpConnector; +import org.kohsuke.github.connector.GitHubConnector; +import org.kohsuke.github.connector.GitHubConnectorRequest; +import org.kohsuke.github.connector.GitHubConnectorResponse; +import org.kohsuke.github.extras.OkHttp3Connector; +import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector; +import org.kohsuke.github.internal.GitHubConnectorHttpConnectorAdapter; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -16,6 +36,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.client.RestTemplate; @@ -25,6 +46,7 @@ import run.halo.app.cache.RedisCacheStore; import run.halo.app.config.attributeconverter.AttributeConverterAutoGenerateConfiguration; import run.halo.app.config.properties.HaloProperties; +import run.halo.app.exception.ServiceException; import run.halo.app.repository.base.BaseRepositoryImpl; import run.halo.app.utils.HttpClientUtils; @@ -49,7 +71,7 @@ public class HaloConfiguration { private final StringRedisTemplate stringRedisTemplate; public HaloConfiguration(HaloProperties haloProperties, - StringRedisTemplate stringRedisTemplate) { + StringRedisTemplate stringRedisTemplate) { this.haloProperties = haloProperties; this.stringRedisTemplate = stringRedisTemplate; } @@ -70,6 +92,7 @@ RestTemplate httpsRestTemplate(RestTemplateBuilder builder) return httpsRestTemplate; } + @Bean @ConditionalOnMissingBean AbstractStringCacheStore stringCacheStore() { diff --git a/src/main/java/run/halo/app/controller/admin/api/VersionCtrlController.java b/src/main/java/run/halo/app/controller/admin/api/VersionCtrlController.java new file mode 100644 index 0000000000..8005a6d9c2 --- /dev/null +++ b/src/main/java/run/halo/app/controller/admin/api/VersionCtrlController.java @@ -0,0 +1,87 @@ +package run.halo.app.controller.admin.api; + +import io.swagger.annotations.ApiOperation; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import run.halo.app.model.dto.VersionInfoDTO; +import run.halo.app.service.HaloVersionCtrlService; + +/** + * Version Control Controller + * + * @author Chen_Kunqiu + */ +@Slf4j +@RestController +@RequestMapping("/api/admin/version") +public class VersionCtrlController { + @Autowired + HaloVersionCtrlService versionCtrlService; + + @GetMapping("releases") + @ApiOperation("Lists all release info of halo jar") + List getAllReleaseInfo() { + return versionCtrlService.getAllReleasesInfo(); + } + + @GetMapping("releases/latest") + @ApiOperation("Lists the latest release info of halo") + VersionInfoDTO getLatestReleaseInfo() { + return versionCtrlService.getLatestReleaseInfo(); + } + + @GetMapping("releases/tags/{tagName:.+}") + @ApiOperation("Lists the release info with specified version") + VersionInfoDTO getReleaseInfo(@PathVariable(name = "tagName") String tagName) { + return versionCtrlService.getReleaseInfoByTag(tagName); + } + + @GetMapping("download/latest") + @ApiOperation("Downloads the latest release jar") + String downloadLatest() { + versionCtrlService.downloadLatestJar(); + return "success"; + } + + @GetMapping("download/tags/{tagName:.+}") + @ApiOperation("Downloads the specified jar") + String download(@PathVariable(name = "tagName") String tagName) { + versionCtrlService.downloadSpecifiedJarToRepo(tagName); + return "success"; + } + + @GetMapping("switch/latest") + @ApiOperation("Switch halo to latest version") + String switchLatestVersion() { + versionCtrlService.switchLatest(); + return "success"; + } + + @GetMapping("switch/tags/{tagName:.+}") + @ApiOperation("Switch halo to specified version") + String switchVersion(@PathVariable(name = "tagName") String tagName) { + versionCtrlService.switchVersion(tagName); + return "success"; + } + + @GetMapping("downloadswitch/latest") + @ApiOperation("Downloads latest version to local and switch halo to it") + String downloadSwitchLatestVersion() { + versionCtrlService.downloadAndSwitchLatest(); + return "success"; + } + + @GetMapping("downloadswitch/tags/{tagName:.+}") + @ApiOperation("Downloads specified version to local and switch halo to it") + String downLoadSwitchVersion(@PathVariable(name = "tagName") String tagName) { + versionCtrlService.downloadAndSwitch(tagName); + return "success"; + } + + +} diff --git a/src/main/java/run/halo/app/controller/content/MainController.java b/src/main/java/run/halo/app/controller/content/MainController.java index 70e2437d63..5b5ac12ffc 100644 --- a/src/main/java/run/halo/app/controller/content/MainController.java +++ b/src/main/java/run/halo/app/controller/content/MainController.java @@ -10,6 +10,7 @@ import run.halo.app.exception.ServiceException; import run.halo.app.model.entity.User; import run.halo.app.model.properties.BlogProperties; +import run.halo.app.model.properties.PrimaryProperties; import run.halo.app.model.support.HaloConst; import run.halo.app.service.OptionService; import run.halo.app.service.UserService; @@ -63,10 +64,14 @@ public String version() { @GetMapping("install") public void installation(HttpServletResponse response) throws IOException { - String installRedirectUri = - StringUtils.appendIfMissing(this.haloProperties.getAdminPath(), "/") - + INSTALL_REDIRECT_URI; - response.sendRedirect(installRedirectUri); + boolean isInstalled = optionService + .getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false); + if (!isInstalled) { + String installRedirectUri = + StringUtils.appendIfMissing(this.haloProperties.getAdminPath(), "/") + + INSTALL_REDIRECT_URI; + response.sendRedirect(installRedirectUri); + } } @GetMapping("avatar") diff --git a/src/main/java/run/halo/app/handler/file/LocalFileHandler.java b/src/main/java/run/halo/app/handler/file/LocalFileHandler.java index 64360fa0bf..7919a5e57c 100644 --- a/src/main/java/run/halo/app/handler/file/LocalFileHandler.java +++ b/src/main/java/run/halo/app/handler/file/LocalFileHandler.java @@ -152,7 +152,8 @@ public UploadResult upload(@NonNull MultipartFile file) { file.getOriginalFilename(), uploadFilePath.getFullPath()); return uploadResult; } catch (IOException e) { - throw new FileOperationException("上传附件失败").setErrorData(uploadFilePath.getFullPath()); + throw new FileOperationException("上传附件失败", e) + .setErrorData(uploadFilePath.getFullPath()); } } diff --git a/src/main/java/run/halo/app/listener/comment/CommentEventListener.java b/src/main/java/run/halo/app/listener/comment/CommentEventListener.java index bba9913b82..779233afb2 100644 --- a/src/main/java/run/halo/app/listener/comment/CommentEventListener.java +++ b/src/main/java/run/halo/app/listener/comment/CommentEventListener.java @@ -4,9 +4,9 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; import run.halo.app.event.comment.CommentNewEvent; import run.halo.app.event.comment.CommentReplyEvent; import run.halo.app.exception.ServiceException; @@ -88,12 +88,12 @@ public CommentEventListener(MailService mailService, OptionService optionService } /** - * Received a new new comment event. + * Received a new comment event. * * @param newEvent new comment event. */ @Async - @EventListener + @TransactionalEventListener public void handleCommentNewEvent(CommentNewEvent newEvent) { Boolean newCommentNotice = optionService .getByPropertyOrDefault(CommentProperties.NEW_NOTICE, Boolean.class, false); @@ -181,7 +181,7 @@ public void handleCommentNewEvent(CommentNewEvent newEvent) { * @param replyEvent reply comment event. */ @Async - @EventListener + @TransactionalEventListener public void handleCommentReplyEvent(CommentReplyEvent replyEvent) { Boolean replyCommentNotice = optionService .getByPropertyOrDefault(CommentProperties.REPLY_NOTICE, Boolean.class, false); diff --git a/src/main/java/run/halo/app/model/dto/VersionInfoDTO.java b/src/main/java/run/halo/app/model/dto/VersionInfoDTO.java new file mode 100644 index 0000000000..53ee2ad546 --- /dev/null +++ b/src/main/java/run/halo/app/model/dto/VersionInfoDTO.java @@ -0,0 +1,52 @@ +package run.halo.app.model.dto; + +import java.io.IOException; +import lombok.Builder; +import lombok.Data; +import lombok.ToString; +import org.kohsuke.github.GHAsset; +import org.kohsuke.github.GHRelease; +import run.halo.app.exception.ServiceException; + +/** + * Version information of a release. + * + *

This is a simplified representation of + * {@linkplain org.kohsuke.github.GHRelease}. + * + * @author Chen_Kunqiu + */ +@Data +@ToString +@Builder +public class VersionInfoDTO { + private String version; + private String jarName; + private String desc; + private String githubUrl; + private String downloadUrl; + private Boolean inLocal; + private Long size; + + /** + * Initially convert the JSON given by Github into VO. + * + * @param release the json data given by github api + * @return the simplified VO object + */ + public static VersionInfoDTO convertFrom(GHRelease release) { + final VersionInfoDTO versionInfoDTO = + VersionInfoDTO.builder().version(release.getTagName()).desc(release.getBody()) + .githubUrl(release.getHtmlUrl().toString()) + .jarName("halo.jar").build(); + try { + final GHAsset asset = release.listAssets().iterator().next(); + versionInfoDTO.setJarName(asset.getName()); + versionInfoDTO.setSize(asset.getSize()); + versionInfoDTO.setDownloadUrl(asset.getBrowserDownloadUrl()); + } catch (IOException e) { + throw new ServiceException("This release has no assert."); + } + return versionInfoDTO; + } +} diff --git a/src/main/java/run/halo/app/model/entity/BasePost.java b/src/main/java/run/halo/app/model/entity/BasePost.java index 94a480fc25..de485fe498 100644 --- a/src/main/java/run/halo/app/model/entity/BasePost.java +++ b/src/main/java/run/halo/app/model/entity/BasePost.java @@ -143,7 +143,7 @@ public class BasePost extends BaseEntity { private Integer topPriority; /** - * Likes + * Likes. */ @Column(name = "likes") @ColumnDefault("0") @@ -169,7 +169,7 @@ public class BasePost extends BaseEntity { private String metaDescription; /** - * Content word count + * Content word count. */ @Column(name = "word_count") @ColumnDefault("0") @@ -188,6 +188,7 @@ public class BasePost extends BaseEntity { @Transient private PatchedContent content; + @Override public void prePersist() { super.prePersist(); @@ -243,6 +244,7 @@ public void prePersist() { if (version == null || version < 0) { version = 1; } + // Clear the value of the deprecated attributes this.originalContent = ""; this.formatContent = ""; diff --git a/src/main/java/run/halo/app/model/enums/SystemType.java b/src/main/java/run/halo/app/model/enums/SystemType.java new file mode 100644 index 0000000000..758634d381 --- /dev/null +++ b/src/main/java/run/halo/app/model/enums/SystemType.java @@ -0,0 +1,10 @@ +package run.halo.app.model.enums; + +/** + * Represents the type of operating system. + * + * @author Chen_Kunqiu + */ +public enum SystemType { + WINDOWS, LINUX, MACOS, ELSE +} diff --git a/src/main/java/run/halo/app/service/HaloVersionCtrlService.java b/src/main/java/run/halo/app/service/HaloVersionCtrlService.java new file mode 100644 index 0000000000..4a1441e39b --- /dev/null +++ b/src/main/java/run/halo/app/service/HaloVersionCtrlService.java @@ -0,0 +1,123 @@ +package run.halo.app.service; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.springframework.scheduling.annotation.Async; +import run.halo.app.model.dto.VersionInfoDTO; + + +/** + * The service to control halo version. + * + * @author Chen_Kunqiu + */ +public interface HaloVersionCtrlService { + + + /** + * check whether the halo jar of the specified version exists in local repository. + * + * @param tagName the specified version + * @return exist in local or not + */ + boolean isInLocal(String tagName); + + /** + * Get all the release info of halo through github api. + * + * @return List of release info organized in json. + */ + List getAllReleasesInfo(); + + /** + * Get specified release info by tagName of the release. + * + * @param tagName the tag name of a github release + * @return single release info + */ + VersionInfoDTO getReleaseInfoByTag(String tagName); + + /** + * Get the release info of the latest release. + * + * @return the release info + */ + VersionInfoDTO getLatestReleaseInfo(); + + /** + * Download the specified jar by tagName into local repository. + * + * @param tagName the specified tag name of the release + */ + @Async + void downloadSpecifiedJarToRepo(String tagName); + + + /** + * Download the latest halo jar into local repository. + * + * @return the version of downloaded jar. + */ + @Async + CompletableFuture downloadLatestJar(); + + /** + * Switch the version of the running halo app into the specified version. + * + *

The general mechanism is shown as follows. + *

    + *
  1. Get the specified halo jar
  2. + *
      + *
    • If found in local repository, copy it into work directory.
    • + *
    • If not found, download it from github into work directory.
    • + *
    + *
  3. Backup the current halo jar into halo-ori-bak.jar.
  4. + *
  5. Construct the same command which launched the current halo jar, + * and modify the target halo jar in the command into the new version halo jar.
  6. + *
  7. Register a JVM exit hook which use the constructed command to + * launch new version halo jar in subprocess.
  8. + *
  9. Terminate current JVM and trigger relevant hook.
  10. + *
  11. After original halo app exit, delete the original halo jar in few seconds.
  12. + *
+ * + * @param tagName the version to switch + */ + @Async + void switchVersion(String tagName); + + + /** + * Switch version of the running halo app into latest. + * + *

If specified version not exist in local repository, + * directly download it into user dir. + */ + @Async + void switchLatest(); + + + /** + * Similar to {@linkplain #switchVersion switchVersion}, but the halo jar would be downloaded + * into local repository. + * + * @param tagName the version to download and switch + */ + @Async + void downloadAndSwitch(String tagName); + + /** + * Similar to {@linkplain #switchLatest switchLatest}, but the halo jar would be downloaded + * into local repository. + */ + @Async + void downloadAndSwitchLatest(); + + /** + * Get the version of current running halo app. + * + * @return the current halo version + */ + String getCurVersion(); + + +} diff --git a/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java b/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java index 157d13316d..5e148a2c08 100644 --- a/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java @@ -64,6 +64,10 @@ public abstract class BasePostServiceImpl private static final Pattern BLANK_PATTERN = Pattern.compile("\\s"); + private static final String CHINESE_REGEX = "[^\\x00-\\xff]"; + + private static final String PUNCTUATION_REGEX = "[\\p{P}\\p{S}\\p{Z}\\s]+"; + public BasePostServiceImpl(BasePostRepository basePostRepository, OptionService optionService, ContentService contentService, @@ -301,7 +305,6 @@ public POST createOrUpdateBy(POST post) { PatchedContent postContent = post.getContent(); // word count stat post.setWordCount(htmlFormatWordCount(postContent.getContent())); - POST savedPost; // Create or update post if (ServiceUtils.isEmptyId(post.getId())) { @@ -484,7 +487,7 @@ protected void generateAndSetSummaryIfAbsent(POST } } - // CS304 issue link : https://github.com/halo-dev/halo/issues/1224 + // CS304 issue link : https://github.com/halo-dev/halo/issues/1759 /** * @param htmlContent the markdown style content @@ -498,6 +501,39 @@ public static long htmlFormatWordCount(String htmlContent) { String cleanContent = HaloUtils.cleanHtmlTag(htmlContent); + String tempString = cleanContent.replaceAll(CHINESE_REGEX, ""); + + String otherString = cleanContent.replaceAll(CHINESE_REGEX, " "); + + int chineseWordCount = cleanContent.length() - tempString.length(); + + String[] otherWords = otherString.split(PUNCTUATION_REGEX); + + int otherWordLength = otherWords.length; + + if (otherWordLength > 0 && otherWords[0].length() == 0) { + otherWordLength--; + } + + if (otherWords.length > 1 && otherWords[otherWords.length - 1].length() == 0) { + otherWordLength--; + } + + return chineseWordCount + otherWordLength; + } + + /** + * @param htmlContent the markdown style content + * @return character count except space and line separator + */ + + public static long htmlFormatCharacterCount(String htmlContent) { + if (htmlContent == null) { + return 0; + } + + String cleanContent = HaloUtils.cleanHtmlTag(htmlContent); + Matcher matcher = BLANK_PATTERN.matcher(cleanContent); int count = 0; diff --git a/src/main/java/run/halo/app/service/impl/HaloVersionCtrlServiceImpl.java b/src/main/java/run/halo/app/service/impl/HaloVersionCtrlServiceImpl.java new file mode 100644 index 0000000000..3eec29611f --- /dev/null +++ b/src/main/java/run/halo/app/service/impl/HaloVersionCtrlServiceImpl.java @@ -0,0 +1,476 @@ +package run.halo.app.service.impl; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import org.apache.commons.lang3.SystemUtils; +import org.jetbrains.annotations.NotNull; +import org.kohsuke.github.GHRelease; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHubBuilder; +import org.kohsuke.github.GitHubRateLimitHandler; +import org.kohsuke.github.connector.GitHubConnectorResponse; +import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import run.halo.app.exception.ServiceException; +import run.halo.app.model.dto.VersionInfoDTO; +import run.halo.app.model.support.HaloConst; +import run.halo.app.service.HaloVersionCtrlService; +import run.halo.app.utils.VmUtils; + +/** + * Halo version control service implementation. + * + * @author Chen_Kunqiu + */ +@Service +@Slf4j +public class HaloVersionCtrlServiceImpl implements HaloVersionCtrlService, ApplicationContextAware { + + private GHRepository haloRepo; + + public static final String GITHUB_RELEASES_API = + "https://api.github.com/repos/halo-dev/halo/releases"; + public static final String GITHUB_RELEASES_LATEST_API = + "https://api.github.com/repos/halo-dev/halo/releases/latest"; + public static final String GITHUB_RELEASES_TAG_API_BASE = + "https://api.github.com/repos/halo-dev/halo/releases/tags/"; + /** + * The dir of local repository for halo jars of diverse versions. + */ + public static final String REPO_DIR = ".jar"; + + /** + * The directory where user launch the JVM. + */ + private static final Path USER_DIR = Paths.get(VmUtils.getUserDir()); + + /** + * The directory where current running halo-jar exists. + */ + private static final Path JAR_DIR = VmUtils.CURR_JAR_DIR; + + + private ApplicationContext context; + + @Autowired + private RestTemplate restTemplate; + + + public HaloVersionCtrlServiceImpl(ApplicationContext context, + RestTemplate restTemplate) { + this.context = context; + this.restTemplate = restTemplate; + try { + haloRepo = connect2github(); + } catch (ServiceException e) { + haloRepo = null; + } + } + + @Override + public boolean isInLocal(String tagName) { + final Path dir = JAR_DIR.resolve(REPO_DIR); + if (!Files.exists(dir)) { + return false; + } + try { + Path target = Files.list(dir).filter(i -> + !StringUtils.hasText(tagName) || i.getFileName().toString().equals(tagName)) + .findFirst().orElse(null); + if (target == null) { + return false; + } + target = + Files.list(target).filter(i -> i.toString().endsWith(".jar")).findFirst() + .orElse(null); + if (target == null) { + return false; + } + } catch (IOException e) { + throw new ServiceException("读取本地目录异常"); + } + + return true; + } + + @Override + public List getAllReleasesInfo() { + + try { + if (haloRepo == null) { + haloRepo = connect2github(); + } + final List releases = haloRepo.listReleases().toList(); + return releases.stream().map(data -> { + final VersionInfoDTO versionInfo = VersionInfoDTO.convertFrom(data); + versionInfo.setInLocal(isInLocal(data.getTagName())); + return versionInfo; + }).collect(Collectors.toList()); + } catch (IOException e) { + throw new ServiceException("从github api中拉取版本信息失败, url: " + GITHUB_RELEASES_API); + } + + } + + @Override + public VersionInfoDTO getReleaseInfoByTag(String tagName) { + if (!tagName.startsWith("v")) { + tagName = "v" + tagName; + } + try { + if (haloRepo == null) { + haloRepo = connect2github(); + } + final GHRelease release = haloRepo.getReleaseByTagName(tagName); + final VersionInfoDTO versionInfo = VersionInfoDTO.convertFrom(release); + versionInfo.setInLocal(isInLocal(tagName)); + return versionInfo; + } catch (IOException e) { + String url = GITHUB_RELEASES_TAG_API_BASE + tagName; + throw new ServiceException("从github api中拉取版本信息失败, url: " + url); + } + } + + @Override + public VersionInfoDTO getLatestReleaseInfo() { + // final RestTemplate restTemplate = builder.build(); + try { + if (haloRepo == null) { + haloRepo = connect2github(); + } + final GHRelease release = haloRepo.getLatestRelease(); + final VersionInfoDTO versionInfo = VersionInfoDTO.convertFrom(release); + versionInfo.setInLocal(isInLocal(release.getTagName())); + return versionInfo; + } catch (IOException e) { + throw new ServiceException("从github api中拉取版本信息失败, url: " + GITHUB_RELEASES_LATEST_API); + } + + } + + @Override + public void downloadSpecifiedJarToRepo(String tagName) { + if (!tagName.startsWith("v")) { + tagName = "v" + tagName; + } + if (isInLocal(tagName)) { + return; + } + final Path dstDir = Paths.get(REPO_DIR).resolve(tagName); + if (!Files.exists(dstDir)) { + try { + Files.createDirectories(dstDir); + } catch (IOException e) { + throw new ServiceException("创建本地仓库失败, 仓库路径: " + dstDir); + } + } + downloadSpecifiedJar(tagName, dstDir.toString()); + } + + + @Override + public CompletableFuture downloadLatestJar() { + final VersionInfoDTO info = getLatestReleaseInfo(); + log.info("Downloading the jar with tagName: {}", info.getVersion()); + downloadSpecifiedJarToRepo(info.getVersion()); + return CompletableFuture.completedFuture(info.getVersion()); + } + + @Override + public void switchVersion(String tagName) { + if (!tagName.startsWith("v")) { + tagName = "v" + tagName; + } + // if the current version equals to the specified version, then return. + if (tagName.equals("v" + getCurVersion())) { + return; + } + final Path curJar = VmUtils.CURR_JAR; + // If not launched in jar, return. + if (!curJar.toString().endsWith(".jar")) { + return; + } + // If local storage exist specified jar, copy to work dir and get the Path of the copy. + Path target; + try { + if (isInLocal(tagName)) { + target = copyTargetFromLocal(tagName); + } else { + // else, directly download the specified through network into work dir + // and get the Path of it. + target = copyTargetFromRemote(tagName); + } + if (target == null) { + throw new IOException(); + } + } catch (IOException e) { + throw new ServiceException("获取目标版本的halo jar包失败, tagName: " + tagName); + } + + final Path backupTarget = JAR_DIR.resolve("halo-ori-bak.jar"); + log.info("Path of target jar get: {}", target); + // backup and delete original jar + try { + backupAndDeleteSpecifiedJar(curJar, backupTarget); + } catch (IOException e) { + throw new ServiceException("备份失败, 备份路径: " + backupTarget); + } + log.info("backup finish: {}", backupTarget); + + // launch the new version jar with the same arguments + startNewVersionApp(target); + // Close the Spring app + int exitCode = SpringApplication.exit(context, () -> 0); + + System.exit(exitCode); + } + + @Override + public void downloadAndSwitch(String tagName) { + downloadSpecifiedJarToRepo(tagName); + switchVersion(tagName); + } + + @Override + public void downloadAndSwitchLatest() { + log.info("latest jar downloading"); + String tagName; + try { + tagName = downloadLatestJar().get(); + } catch (InterruptedException | ExecutionException e) { + throw new ServiceException("下载失败"); + } + log.info("latest jar download successfully: {}", tagName); + switchVersion(tagName); + } + + @Override + public String getCurVersion() { + return HaloConst.HALO_VERSION; + } + + @Override + public void switchLatest() { + final String tagName = getLatestReleaseInfo().getVersion(); + switchVersion(tagName); + } + + @Override + public void setApplicationContext(@NotNull ApplicationContext applicationContext) { + context = applicationContext; + } + + private GHRepository connect2github() { + try { + log.info("Connect to Github."); + final OkHttpClient httpClient = + new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS).build(); + return new GitHubBuilder() + .withConnector(new OkHttpGitHubConnector(httpClient)) + .withRateLimitHandler(new GitHubRateLimitHandler() { + @Override + public void onError(@NotNull GitHubConnectorResponse githubResp) + throws IOException { + throw new ServiceException("无法访问Github API,可能访问频次过高. URL: " + + githubResp.request().url()); + } + }) + .build().getRepository("halo-dev/halo"); + } catch (IOException e) { + throw new ServiceException("无法连接Github,请检查网络."); + } + } + + /** + * Download the specified jar by tagName into the given destination directory. + * + * @param tagName the specified tag name of the release + * @param dstDir destination directory + * @return the final path of the downloaded jar + */ + private String downloadSpecifiedJar(String tagName, String dstDir) { + final VersionInfoDTO releaseInfo = + Objects.requireNonNull(getReleaseInfoByTag(tagName)); + final String jarUrl = releaseInfo.getDownloadUrl(); + final String jarName = releaseInfo.getJarName(); + Assert.hasText(jarUrl, "Jar url must not be blank"); + Path target = Paths.get(dstDir).resolve(jarName); + download(jarUrl, target); + return target.toString(); + } + + + /** + * Download resource to specified file. + * + *

As the jar file is very big, it is not appropriate to load it as byte[] in memory, + * so that directly forwarded it to the file system. + * + * @param url the url to download resource + * @param tarFile target file + */ + private void download(String url, Path tarFile) { + restTemplate.execute(url, HttpMethod.GET, null, + resp -> { + log.info("Downloading [{}]", url); + try (final BufferedInputStream is = new BufferedInputStream(resp.getBody()); + final BufferedOutputStream os = new BufferedOutputStream( + new FileOutputStream(tarFile.toFile()))) { + is.transferTo(os); + } catch (IOException e) { + throw new ServiceException("下载失败 " + + url + + ", 状态码: " + + resp.getStatusCode()); + } + return tarFile; + }); + } + + + /** + * Copy the target jar file in local repo with specified tagName to work dir + * and get the Path of the copy. + * + * @param tagName version of target jar, such as v1.1, v2.2.1... + * @return the Path of target jar file + * @throws IOException exception may happen in copying. + */ + private Path copyTargetFromLocal(String tagName) throws IOException { + final Path dir = JAR_DIR.resolve(REPO_DIR); + Path target = + Files.list(dir).filter(i -> + !StringUtils.hasText(tagName) || i.getFileName().toString().equals(tagName)) + .findFirst().orElse(null); + if (target == null) { + return null; + } + target = + Files.list(target).filter(i -> i.toString().endsWith(".jar")).findFirst().orElse(null); + if (target == null) { + return null; + } + final Path newJar = JAR_DIR.resolve(target.getFileName()); + return Files.copy(target, newJar, StandardCopyOption.REPLACE_EXISTING); + } + + + /** + * Download the target jar file in github repo with specified tagName to work dir + * and get the Path of the target jar. + * + * @param tagName version of target jar, such as v1.1, v2.2.1... + * @return the Path of target jar file + */ + private Path copyTargetFromRemote(String tagName) { + return Paths.get(downloadSpecifiedJar(tagName, JAR_DIR.toString())); + } + + + /** + * Backup and delete the specified jar file. + * The implementation in Windows differs as the file lock in Windows. + * + * @param curJar the specified jar path + * @param backupTarget the path of the backup + * @throws IOException the exception may arise in the process of backup + */ + private void backupAndDeleteSpecifiedJar(Path curJar, Path backupTarget) throws IOException { + final Path backupDir = backupTarget.getParent(); + assert backupDir != null; + Files.createDirectories(backupDir); + Files.copy(curJar, backupTarget, StandardCopyOption.REPLACE_EXISTING); + ProcessBuilder pb = new ProcessBuilder(); + pb.directory(USER_DIR.toFile()); + if (SystemUtils.IS_OS_WINDOWS) { + pb.command("cmd", "/c", + "ping localhost -n 10 > nul && del " + curJar.toAbsolutePath()); + } else if (SystemUtils.IS_OS_LINUX) { + /* + * On Unix-like operating systems, there is no file locking, + * thus you can directly change the name of the current JAR. + * If you do so, however, `SpringApplication.exit` will not + * terminate the program properly + * */ + pb.command("sh", "-c", "sleep 10s && rm -f " + curJar.toAbsolutePath()); + } else if (SystemUtils.IS_OS_MAC) { + /* + * On Unix-like operating systems, there is no file locking, + * thus you can directly change the name of the current JAR. + * If you do so, however, `SpringApplication.exit` will not + * terminate the program properly + * */ + pb.command("sh", "-c", "sleep 10s && rm -f " + curJar.toAbsolutePath()); + } else { + pb = null; + } + if (pb != null) { + ProcessBuilder finalPb = pb; + Runtime.getRuntime().addShutdownHook(new Thread( + () -> { + try { + finalPb.start(); + } catch (IOException e) { + e.printStackTrace(); + } + } + )); + } + + } + + /** + * Start the selected new version application of halo by launch another process. + * + * @param target the target jar file of new version app + */ + private void startNewVersionApp(Path target) { + final List cmd = VmUtils.getNewLaunchCommand(target.toString()); + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(USER_DIR.toFile()); + log.info("Cmd to launch new version halo app: {}", String.join(" ", cmd)); + // Registers a new virtual-machine shutdown hook to launch new version halo app. + // At the time of JVM exiting, the network resource it owning would be released, + // hence, starting new halo-app at that moment could avoid port conflict. + Runtime.getRuntime().addShutdownHook(new Thread( + () -> { + try { + final Process process = pb.inheritIO().start(); + System.out.println( + "\n------------------------------\n" + + "New process PID: " + process.pid() + "\n" + + "Command to launch: " + String.join(" ", cmd) + + "\n------------------------------\n"); + } catch (IOException e) { + e.printStackTrace(); + } + } + )); + + } + + +} diff --git a/src/main/java/run/halo/app/service/impl/StaticStorageServiceImpl.java b/src/main/java/run/halo/app/service/impl/StaticStorageServiceImpl.java index 341406f0ad..1e1367ced9 100644 --- a/src/main/java/run/halo/app/service/impl/StaticStorageServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/StaticStorageServiceImpl.java @@ -53,7 +53,12 @@ public StaticStorageServiceImpl(HaloProperties haloProperties, @Override public List listStaticFolder() { - return listStaticFileTree(staticDir); + + List staticFiles = listStaticFileTree(staticDir); + + onChange(); // To update the mapping of local files. + + return staticFiles; } @Nullable diff --git a/src/main/java/run/halo/app/utils/VmUtils.java b/src/main/java/run/halo/app/utils/VmUtils.java new file mode 100644 index 0000000000..209dc4f145 --- /dev/null +++ b/src/main/java/run/halo/app/utils/VmUtils.java @@ -0,0 +1,204 @@ +package run.halo.app.utils; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import run.halo.app.exception.ServiceException; + +/** + * The utils to get some info of JVM. + * + * @author Chen_Kunqiu + */ +@Component +public class VmUtils { + private static final RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean(); + public static final Path CURR_JAR; + public static final Path CURR_JAR_DIR; + private static ApplicationArguments appArgs; + + static { + String path = + VmUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + // As the special package form of Spring, the path would start with "file:" + if (path.startsWith("file:")) { + path = path.substring(5); + } + // If not containing ".jar", it is test scenario. + final int idx = path.contains(".jar") ? path.indexOf(".jar") + 4 : path.length(); + + CURR_JAR = new File(path.substring(0, idx)).toPath(); + CURR_JAR_DIR = CURR_JAR.getParent(); + } + + @Autowired + private void setAppArgs(ApplicationArguments appArgs) { + VmUtils.appArgs = appArgs; + } + + /** + * Get the command to launch the halo jar. + * + *

Get the same command as the command to launch this Java program. + * Use this command to restart the Java program after halo updates. + * + * @return the full command + */ + public static List getSameLaunchCommand() { + List cmd = new ArrayList<>(); + cmd.add(getJvmExecutablePath()); + cmd.addAll(getVmOptions()); + cmd.add("-classpath"); + cmd.add(getClassPath()); + cmd.add("-jar"); + cmd.add(getRunningJar()); + cmd.addAll(getProgramArgs()); + return cmd; + } + + /** + * Get the new command to launch halo jar with new version. + * + *

Get the same new launch command as the command to launch this Java program + * except for the Java target to launch. + * Use this command to restart the Java program after halo updates. + * + * @return the full command + */ + public static List getNewLaunchCommand(String newTarget) { + List cmd = new ArrayList<>(); + cmd.add(getJvmExecutablePath()); + cmd.addAll(getVmOptions()); + cmd.add("-classpath"); + String classPath = getClassPath(); + final String nonVmPartOfCmd = getNonVmPartOfCmd(); + final Path fileName = CURR_JAR.getFileName(); + if (fileName == null) { + throw new ServiceException("无法获取当前运行的JAR"); + } + final String jarName = fileName.toString(); + + final int endIdx = nonVmPartOfCmd.indexOf(jarName) + jarName.length(); + /* + * Since cannot determine whether user use relative or absolute path to launch halo.jar, + * and CURR_JAR is absolute path, + * use this approach to obtain the real form of halo jar's path when launching. + */ + String originalJar = nonVmPartOfCmd.substring(0, endIdx); + // replace the class path + classPath = classPath.replace(originalJar, newTarget); + cmd.add(classPath); + cmd.add("-jar"); + cmd.add(newTarget); + cmd.addAll(getProgramArgs()); + return cmd; + } + + /** + * Get the absolute path of java / javaw. + * + *

Get the full path of the JVM executable which runs the current Java program. + * As the JVM is not always specified as JAVA_HOME, cannot get it simply by + * {@code $JAVA_HOME/bin/java} + * + * @return the full path of current JVM + */ + public static String getJvmExecutablePath() { + // need JAVA 9+ + return ProcessHandle.current() + .info() + .command() + .orElseThrow(); + } + + /** + * Get the VM arguments passed to JVM. + * + *

For example,
+ * {@code java -jar -Da=1 Test.jar b=2}
+ * -->
+ * {@code -Da=1} + * + * @return the VM arguments + */ + public static List getVmOptions() { + return RUNTIME_MX_BEAN.getInputArguments(); + } + + + /** + * Get the program arguments passed to JVM. + * + *

For example,
+ * {@code java -jar -Da=1 Test.jar b=2}
+ * -->
+ * {@code b=2} + * + * @return the VM arguments + */ + public static List getProgramArgs() { + return Arrays.asList(appArgs.getSourceArgs()); + } + + /** + * Get the class path which the JVM relies on. + * + * @return the class path + */ + public static String getClassPath() { + return RUNTIME_MX_BEAN.getClassPath(); + } + + /** + * Get the program arguments including the target class or jar. + * + *

For example,
+ * {@code java -jar -Da=1 Test.jar b=2}
+ * -->
+ * {@code Test.jar b=2} + * + * @return the program arguments including the target class or jar + */ + public static String getNonVmPartOfCmd() { + return System.getProperty("sun.java.command"); + } + + /** + * Get the full path of the running jar. + * + *

If running class file without jar, then return the directory contains the root package. + * + * @return the full path of running jar. + */ + public static String getRunningJar() { + return CURR_JAR.toString(); + } + + /** + * Get the full path of the directory of the running jar. + * + * @return the full path of the directory of the running jar. + */ + public static String getRunningJarDir() { + return CURR_JAR_DIR.toString(); + } + + /** + * Get the work directory of JVM. + * + * @return work directory (aka. user dir) + */ + public static String getUserDir() { + return System.getProperty("user.dir"); + } + + + +} diff --git a/src/test/java/run/halo/app/service/impl/HTMLWordCountTest.java b/src/test/java/run/halo/app/service/impl/HTMLWordCountTest.java index 6d6d7d50e5..3624663914 100644 --- a/src/test/java/run/halo/app/service/impl/HTMLWordCountTest.java +++ b/src/test/java/run/halo/app/service/impl/HTMLWordCountTest.java @@ -59,6 +59,20 @@ public class HTMLWordCountTest { String emptyString = ""; + String englishString = "I have a red apple"; + + String hybridString = "I have a red apple哈哈"; + + + String complexText2 = "Hi,Jessica!这个project的schedule有些问题。"; + + String complexText3 = "The company had a meeting yesterday。Why did you ask for leave?"; + + String complexText4 = "这是一个句子,但是只有中文。"; + + String complexText5 = + "The wind and the moon are all beautiful, love and hate are all romantic."; + @Test void pictureTest() { assertEquals("图片字数测试".length(), @@ -128,4 +142,42 @@ void emptyTest() { assertEquals(0, BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(emptyString))); } -} + + @Test + void englishTest() { + assertEquals(5, + BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(englishString))); + } + + @Test + void hybridTest() { + assertEquals(7, + BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(hybridString))); + } + + @Test + void englishCharacterTest() { + assertEquals(14, + BasePostServiceImpl.htmlFormatCharacterCount(MarkdownUtils.renderHtml(englishString))); + } + + @Test + void hybridCharacterTest() { + assertEquals(16, + BasePostServiceImpl.htmlFormatCharacterCount(MarkdownUtils.renderHtml(hybridString))); + } + + @Test + void moreComplexTest() { + assertEquals(14, + BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(complexText2))); + assertEquals(14, + BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(complexText3))); + assertEquals(14, + BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(complexText4))); + assertEquals(14, + BasePostServiceImpl.htmlFormatWordCount(MarkdownUtils.renderHtml(complexText5))); + } + + +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/service/impl/HaloVersionCtrlServiceImplTest.java b/src/test/java/run/halo/app/service/impl/HaloVersionCtrlServiceImplTest.java new file mode 100644 index 0000000000..6b56f75df3 --- /dev/null +++ b/src/test/java/run/halo/app/service/impl/HaloVersionCtrlServiceImplTest.java @@ -0,0 +1,114 @@ +package run.halo.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.platform.commons.util.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpMethod; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import run.halo.app.exception.ServiceException; +import run.halo.app.model.dto.VersionInfoDTO; +import run.halo.app.model.support.HaloConst; +import run.halo.app.service.HaloVersionCtrlService; +import run.halo.app.utils.VmUtils; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +class HaloVersionCtrlServiceImplTest { + + @Autowired + private RestTemplate restTemplate; + + @Autowired + HaloVersionCtrlService versionCtrlService; + + + + @Test + void testIsInLocal() { + assertFalse(this.versionCtrlService.isInLocal("Tag Name")); + Path dir = VmUtils.CURR_JAR_DIR; + Path repo = dir.resolve(HaloVersionCtrlServiceImpl.REPO_DIR); + Path test = null; + Path tempFile = null; + final boolean exists = Files.exists(repo); + try { + if (!exists) { + Files.createDirectories(repo); + } + test = Files.createTempDirectory(repo, "test"); + assertFalse(versionCtrlService.isInLocal(test.getFileName().toString())); + tempFile = Files.createTempFile(test, "halo", ".jar"); + assertTrue(versionCtrlService.isInLocal(test.getFileName().toString())); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (tempFile != null) { + Files.deleteIfExists(tempFile); + } + if (test != null) { + Files.deleteIfExists(test); + } + if (!exists) { + Files.deleteIfExists(repo); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + @Test + void testGetLatestReleaseInfo() throws RestClientException { + + final VersionInfoDTO latestReleaseInfo = versionCtrlService.getLatestReleaseInfo(); + + assertNotNull(latestReleaseInfo); + assertFalse(StringUtils.isBlank(latestReleaseInfo.getDownloadUrl())); + } + + @Test + void testDownload() throws RestClientException { + + // 仅仅分片下载 10 bytes以测试下载通道是否顺畅 + assertDoesNotThrow(() -> { + final VersionInfoDTO latestReleaseInfo = versionCtrlService.getLatestReleaseInfo(); + final String downloadUrl = latestReleaseInfo.getDownloadUrl(); + + final byte[] data = restTemplate.execute(downloadUrl, HttpMethod.GET, + + req -> req.getHeaders() + .set("Range", String.format("bytes=%d-%d", 0, 9)), + resp -> { + try { + return resp.getBody().readAllBytes(); + } catch (Exception e) { + throw new ServiceException("下载失败, url: " + downloadUrl); + } + }); + assertNotNull(data); + assertEquals(10, data.length); + } + ); + + } + + @Test + void testGetCurVersion() { + assertEquals(HaloConst.HALO_VERSION, versionCtrlService.getCurVersion()); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/utils/VmUtilsTest.java b/src/test/java/run/halo/app/utils/VmUtilsTest.java new file mode 100644 index 0000000000..3956f8025a --- /dev/null +++ b/src/test/java/run/halo/app/utils/VmUtilsTest.java @@ -0,0 +1,97 @@ +package run.halo.app.utils; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.util.StringUtils; + +class VmUtilsTest { + private static RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean(); + + @Test + void testGetJvmExecutablePath() { + String actualJvmExecutablePath = VmUtils.getJvmExecutablePath(); + assertTrue(StringUtils.isNotBlank(actualJvmExecutablePath)); + assertDoesNotThrow(() -> { + new ProcessBuilder(actualJvmExecutablePath).start(); + }); + } + + @Test + void testGetJvmExecutablePath2() { + String actualJvmExecutablePath = VmUtils.getJvmExecutablePath(); + assertTrue(actualJvmExecutablePath.matches(".*java.*")); + } + + + @Test + void testGetVmArguments() { + final List vmArguments = VmUtils.getVmOptions(); + assertNotNull(vmArguments); + } + + @Test + void testGetVmArguments2() { + final List vmArguments = VmUtils.getVmOptions(); + assertEquals(bean.getInputArguments(), vmArguments); + } + + @Test + void testGetClassPath() { + String actualClassPath = VmUtils.getClassPath(); + assertEquals(bean.getClassPath(), actualClassPath); + } + + @Test + void testGetClassPath2() { + String actualClassPath = VmUtils.getClassPath(); + final String separator = System.getProperty("path.separator"); + final String[] classSource = actualClassPath.split(separator); + for (String s : classSource) { + assertTrue(new File(s).exists()); + } + } + + @Test + void testGetNonVmPartOfCmd() { + assertEquals(System.getProperty("sun.java.command"), VmUtils.getNonVmPartOfCmd()); + } + + @Test + void testGetNonVmPartOfCmd2() { + final String args = VmUtils.getNonVmPartOfCmd(); + assertTrue(StringUtils.isNotBlank(args)); + } + + @Test + void testGetRunningJar() { + final String runningJar = VmUtils.getRunningJar(); + assertTrue(StringUtils.isNotBlank(runningJar)); + } + @Test + void testGetRunningJar2() { + final String runningJar = VmUtils.getRunningJar(); + final File file = new File(runningJar); + assertTrue(file.exists()); + } + + @Test + void testGetUserDir() { + assertEquals(System.getProperty("user.dir"), VmUtils.getUserDir()); + } + + @Test + void testGetUserDir2() { + final String userDir = VmUtils.getUserDir(); + assertTrue(new File(userDir).exists()); + } +} +