From eaee0416448b0994b8e3854a4cc7ab39043ee9d4 Mon Sep 17 00:00:00 2001 From: bobo Date: Thu, 14 Nov 2019 09:47:09 +0800 Subject: [PATCH] Batch export markdown backup in hexo format --- .../admin/api/BackupController.java | 58 +++++++++- .../run/halo/app/service/BackupService.java | 18 ++++ .../app/service/impl/BackupServiceImpl.java | 100 +++++++++++++++++- 3 files changed, 172 insertions(+), 4 deletions(-) diff --git a/src/main/java/run/halo/app/controller/admin/api/BackupController.java b/src/main/java/run/halo/app/controller/admin/api/BackupController.java index 6f860121e..5cbd2fabb 100644 --- a/src/main/java/run/halo/app/controller/admin/api/BackupController.java +++ b/src/main/java/run/halo/app/controller/admin/api/BackupController.java @@ -1,16 +1,26 @@ package run.halo.app.controller.admin.api; +import cn.hutool.core.util.ZipUtil; import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.json.JSONObject; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import run.halo.app.exception.FileOperationException; import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.service.BackupService; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; -import java.util.LinkedList; +import java.util.Date; import java.util.List; /** @@ -21,6 +31,7 @@ import java.util.List; */ @RestController @RequestMapping("/api/admin/backups") +@Slf4j public class BackupController { private final BackupService backupService; @@ -34,4 +45,49 @@ public class BackupController { public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException { return backupService.importMarkdown(file); } + + @GetMapping("export/hexo") + @ApiOperation("export hexo markdown") + public void exportMarkdowns(HttpServletResponse response) { + final String tmpDir = System.getProperty("java.io.tmpdir"); + final String date = DateFormatUtils.format(new Date(), "yyyyMMddHHmmss"); + String localFilePath = tmpDir + File.separator + "halo-markdown-" + date; + log.trace(localFilePath); + final File localFile = new File(localFilePath); + final File postDir = new File(localFilePath + File.separator + "posts"); + final File passwordDir = new File(localFilePath + File.separator + "passwords"); + final File draftDir = new File(localFilePath + File.separator + "drafts"); + try { + if (!postDir.mkdirs()) { + throw new Exception("Create dir [" + postDir.getPath() + "] failed"); + } + if (!passwordDir.mkdirs()) { + throw new Exception("Create dir [" + passwordDir.getPath() + "] failed"); + } + if (!draftDir.mkdirs()) { + throw new Exception("Create dir [" + draftDir.getPath() + "] failed"); + } + final JSONObject result = backupService.exportHexoMDs(); + final List posts = (List) result.opt("posts"); + backupService.exportHexoMd(posts, postDir.getPath()); + final List passwords = (List) result.opt("passwords"); + backupService.exportHexoMd(passwords, passwordDir.getPath()); + final List drafts = (List) result.opt("drafts"); + backupService.exportHexoMd(drafts, draftDir.getPath()); + + final File zipFile = ZipUtil.zip(localFile); + byte[] zipData; + try (final FileInputStream inputStream = new FileInputStream(zipFile)) { + zipData = IOUtils.toByteArray(inputStream); + response.setContentType("application/zip"); + final String fileName = "halo-markdown-" + date + ".zip"; + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + } + + response.getOutputStream().write(zipData); + } catch (final Exception e) { + log.error("Export failed", e); + throw new FileOperationException("文章导出失败", e); + } + } } diff --git a/src/main/java/run/halo/app/service/BackupService.java b/src/main/java/run/halo/app/service/BackupService.java index 48b6973bd..c17a69f0c 100644 --- a/src/main/java/run/halo/app/service/BackupService.java +++ b/src/main/java/run/halo/app/service/BackupService.java @@ -1,9 +1,11 @@ package run.halo.app.service; +import org.json.JSONObject; import org.springframework.web.multipart.MultipartFile; import run.halo.app.model.dto.post.BasePostDetailDTO; import java.io.IOException; +import java.util.List; /** * Backup service interface. @@ -20,4 +22,20 @@ public interface BackupService { * @return post info */ BasePostDetailDTO importMarkdown(MultipartFile file) throws IOException; + + + /** + * export posts by hexo formatter + * + * @return + */ + JSONObject exportHexoMDs(); + + /** + * Exports the specified articles to the specified dir path. + * + * @param posts + * @param path + */ + void exportHexoMd(List posts, String path); } diff --git a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java index 5360e2720..71e50187f 100644 --- a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java @@ -1,14 +1,31 @@ package run.halo.app.service.impl; import cn.hutool.core.io.IoUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateFormatUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import org.yaml.snakeyaml.Yaml; import run.halo.app.model.dto.post.BasePostDetailDTO; +import run.halo.app.model.entity.Post; +import run.halo.app.model.entity.Tag; import run.halo.app.service.BackupService; import run.halo.app.service.PostService; +import run.halo.app.service.PostTagService; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * Backup service implementation. @@ -17,13 +34,15 @@ import java.nio.charset.StandardCharsets; * @date 2019-04-26 */ @Service +@Slf4j +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class BackupServiceImpl implements BackupService { private final PostService postService; - public BackupServiceImpl(PostService postService) { - this.postService = postService; - } + private final PostTagService postTagService; + + public static final String LINE_SEPARATOR = System.getProperty("line.separator"); @Override public BasePostDetailDTO importMarkdown(MultipartFile file) throws IOException { @@ -35,4 +54,79 @@ public class BackupServiceImpl implements BackupService { return postService.importMarkdown(markdown, file.getOriginalFilename()); } + + @Override + public JSONObject exportHexoMDs() { + final JSONObject ret = new JSONObject(); + final List posts = new ArrayList<>(); + ret.put("posts", (Object) posts); + final List passwords = new ArrayList<>(); + ret.put("passwords", (Object) passwords); + final List drafts = new ArrayList<>(); + ret.put("drafts", (Object) drafts); + + List postList = postService.listAll(); + Map> talMap = postTagService.listTagListMapBy(postList.stream().map(Post::getId).collect(Collectors.toList())); + for (Post post : postList) { + final Map front = new LinkedHashMap<>(); + final String title = post.getTitle(); + front.put("title", title); + final String date = DateFormatUtils.format(post.getCreateTime(), "yyyy-MM-dd HH:mm:ss"); + front.put("date", date); + front.put("updated", DateFormatUtils.format(post.getUpdateTime(), "yyyy-MM-dd HH:mm:ss")); + final List tags = talMap.get(post.getId()).stream().map(Tag::getName).collect(Collectors.toList()); + if (tags.isEmpty()) { + tags.add("halo"); + } + front.put("tags", tags); + front.put("permalink", ""); + final JSONObject one = new JSONObject(); + one.put("front", new Yaml().dump(front)); + one.put("title", title); + one.put("content", post.getOriginalContent()); + one.put("created", post.getCreateTime().getTime()); + + if (StringUtils.isNotBlank(post.getPassword())) { + passwords.add(one); + continue; + } else if (post.getDeleted()) { + drafts.add(one); + continue; + } else { + posts.add(one); + } + } + + return ret; + } + + @Override + public void exportHexoMd(List posts, String dirPath) { + posts.forEach(post -> { + final String filename = sanitizeFilename(post.optString("title")) + ".md"; + final String text = post.optString("front") + "---" + LINE_SEPARATOR + post.optString("content"); + + try { + final String date = DateFormatUtils.format(post.optLong("created"), "yyyyMM"); + final String dir = dirPath + File.separator + date + File.separator; + new File(dir).mkdirs(); + FileUtils.writeStringToFile(new File(dir + filename), text, "UTF-8"); + } catch (final Exception e) { + log.error("Write markdown file failed", e); + } + }); + } + + /** + * Sanitizes the specified file name. + * + * @param unsanitized the specified file name + * @return sanitized file name + */ + public static String sanitizeFilename(final String unsanitized) { + return unsanitized. + replaceAll("[^(a-zA-Z0-9\\u4e00-\\u9fa5\\.)]", ""). + replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~]", ""). + replaceAll("\\s", ""); + } }