mirror of https://github.com/halo-dev/halo
feat: add api for markdown export (#1199)
* add API for markdown-export * add front-matter support * optimize fileName for markdown-export * fornt-matter与正文中间增加换行符pull/1220/head
parent
276aea4bdd
commit
8ba115ffb6
|
@ -58,6 +58,11 @@ public class HaloProperties {
|
|||
*/
|
||||
private String backupDir = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "halo-backup" + FILE_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Halo backup markdown directory.(Not recommended to modify this config);
|
||||
*/
|
||||
private String backupMarkdownDir = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "halo-backup-markdown" + FILE_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Halo data export directory.
|
||||
*/
|
||||
|
|
|
@ -12,6 +12,7 @@ import run.halo.app.annotation.DisableOnCondition;
|
|||
import run.halo.app.config.properties.HaloProperties;
|
||||
import run.halo.app.model.dto.BackupDTO;
|
||||
import run.halo.app.model.dto.post.BasePostDetailDTO;
|
||||
import run.halo.app.model.params.PostMarkdownParam;
|
||||
import run.halo.app.service.BackupService;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
@ -23,6 +24,7 @@ import java.util.List;
|
|||
*
|
||||
* @author johnniang
|
||||
* @author ryanwang
|
||||
* @author Raremaa
|
||||
* @date 2019-04-26
|
||||
*/
|
||||
@RestController
|
||||
|
@ -34,8 +36,7 @@ public class BackupController {
|
|||
|
||||
private final HaloProperties haloProperties;
|
||||
|
||||
public BackupController(BackupService backupService,
|
||||
HaloProperties haloProperties) {
|
||||
public BackupController(BackupService backupService, HaloProperties haloProperties) {
|
||||
this.backupService = backupService;
|
||||
this.haloProperties = haloProperties;
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ public class BackupController {
|
|||
backupService.deleteWorkDirBackup(filename);
|
||||
}
|
||||
|
||||
@PostMapping("markdown")
|
||||
@PostMapping("markdown/import")
|
||||
@ApiOperation("Imports markdown")
|
||||
public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException {
|
||||
return backupService.importMarkdown(file);
|
||||
|
@ -132,4 +133,50 @@ public class BackupController {
|
|||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + exportDataResource.getFilename() + "\"")
|
||||
.body(exportDataResource);
|
||||
}
|
||||
|
||||
@PostMapping("markdown/export")
|
||||
@ApiOperation("Exports markdowns")
|
||||
@DisableOnCondition
|
||||
public BackupDTO exportMarkdowns(@RequestBody PostMarkdownParam postMarkdownParam) throws IOException {
|
||||
return backupService.exportMarkdowns(postMarkdownParam);
|
||||
}
|
||||
|
||||
@GetMapping("markdown/export")
|
||||
@ApiOperation("Gets all markdown backups")
|
||||
public List<BackupDTO> listMarkdowns() {
|
||||
return backupService.listMarkdowns();
|
||||
}
|
||||
|
||||
@DeleteMapping("markdown/export")
|
||||
@ApiOperation("Deletes a markdown backup")
|
||||
@DisableOnCondition
|
||||
public void deleteMarkdown(@RequestParam("filename") String filename) {
|
||||
backupService.deleteMarkdown(filename);
|
||||
}
|
||||
|
||||
@GetMapping("markdown/export/{fileName:.+}")
|
||||
@ApiOperation("Downloads a work markdown backup file")
|
||||
@DisableOnCondition
|
||||
public ResponseEntity<Resource> downloadMarkdown(@PathVariable("fileName") String fileName, HttpServletRequest request) {
|
||||
log.info("Try to download markdown backup file: [{}]", fileName);
|
||||
|
||||
// Load file as resource
|
||||
Resource backupResource = backupService.loadFileAsResource(haloProperties.getBackupMarkdownDir(), fileName);
|
||||
|
||||
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
// Try to determine file's content type
|
||||
try {
|
||||
contentType = request.getServletContext().getMimeType(backupResource.getFile().getAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
log.warn("Could not determine file type", e);
|
||||
// Ignore this error
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + backupResource.getFilename() + "\"")
|
||||
.body(backupResource);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package run.halo.app.model.params;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author Raremaa
|
||||
* @date 2020/12/25 11:22 上午
|
||||
*/
|
||||
@Data
|
||||
public class PostMarkdownParam {
|
||||
|
||||
/**
|
||||
* true if need frontMatter
|
||||
* default false
|
||||
*/
|
||||
private Boolean needFrontMatter;
|
||||
}
|
|
@ -36,6 +36,11 @@ public class HaloConst {
|
|||
*/
|
||||
public final static String HALO_BACKUP_PREFIX = "halo-backup-";
|
||||
|
||||
/**
|
||||
* Halo backup markdown prefix.
|
||||
*/
|
||||
public final static String HALO_BACKUP_MARKDOWN_PREFIX = "halo-backup-markdown-";
|
||||
|
||||
/**
|
||||
* Halo data export prefix.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Markdown export VO
|
||||
*
|
||||
* @author Raremaa
|
||||
* @date 2020/12/25 9:14 上午
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
public class PostMarkdownVO {
|
||||
|
||||
private String title;
|
||||
|
||||
private String slug;
|
||||
|
||||
private String originalContent;
|
||||
|
||||
private String frontMatter;
|
||||
}
|
|
@ -5,6 +5,7 @@ import org.springframework.lang.NonNull;
|
|||
import org.springframework.web.multipart.MultipartFile;
|
||||
import run.halo.app.model.dto.BackupDTO;
|
||||
import run.halo.app.model.dto.post.BasePostDetailDTO;
|
||||
import run.halo.app.model.params.PostMarkdownParam;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
@ -91,4 +92,29 @@ public interface BackupService {
|
|||
* @throws IOException throws IOException
|
||||
*/
|
||||
void importData(MultipartFile file) throws IOException;
|
||||
|
||||
/**
|
||||
* Export Markdown content
|
||||
*
|
||||
* @param postMarkdownParam param
|
||||
* @return backup dto.
|
||||
* @throws IOException throws IOException
|
||||
*/
|
||||
@NonNull
|
||||
BackupDTO exportMarkdowns(PostMarkdownParam postMarkdownParam) throws IOException;
|
||||
|
||||
/**
|
||||
* list Markdown backups
|
||||
*
|
||||
* @return backup list
|
||||
*/
|
||||
@NonNull
|
||||
List<BackupDTO> listMarkdowns();
|
||||
|
||||
/**
|
||||
* delete a markdown backup
|
||||
*
|
||||
* @param fileName
|
||||
*/
|
||||
void deleteMarkdown(@NonNull String fileName);
|
||||
}
|
||||
|
|
|
@ -8,10 +8,7 @@ import run.halo.app.model.entity.Post;
|
|||
import run.halo.app.model.entity.PostMeta;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.model.params.PostQuery;
|
||||
import run.halo.app.model.vo.ArchiveMonthVO;
|
||||
import run.halo.app.model.vo.ArchiveYearVO;
|
||||
import run.halo.app.model.vo.PostDetailVO;
|
||||
import run.halo.app.model.vo.PostListVO;
|
||||
import run.halo.app.model.vo.*;
|
||||
import run.halo.app.service.base.BasePostService;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
@ -112,8 +109,8 @@ public interface PostService extends BasePostService<Post> {
|
|||
/**
|
||||
* Gets post by post year and slug.
|
||||
*
|
||||
* @param year post create year.
|
||||
* @param slug post slug.
|
||||
* @param year post create year.
|
||||
* @param slug post slug.
|
||||
* @return post info
|
||||
*/
|
||||
@NonNull
|
||||
|
@ -276,4 +273,12 @@ public interface PostService extends BasePostService<Post> {
|
|||
@NotNull
|
||||
Sort getPostDefaultSort();
|
||||
|
||||
|
||||
/**
|
||||
* Lists PostMarkdown vo
|
||||
*
|
||||
* @return a list of PostMarkdown vo
|
||||
*/
|
||||
@NonNull
|
||||
List<PostMarkdownVO> listPostMarkdowns();
|
||||
}
|
||||
|
|
|
@ -23,10 +23,13 @@ import run.halo.app.event.options.OptionUpdatedEvent;
|
|||
import run.halo.app.event.theme.ThemeUpdatedEvent;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.exception.ServiceException;
|
||||
import run.halo.app.handler.file.FileHandler;
|
||||
import run.halo.app.model.dto.BackupDTO;
|
||||
import run.halo.app.model.dto.post.BasePostDetailDTO;
|
||||
import run.halo.app.model.entity.*;
|
||||
import run.halo.app.model.params.PostMarkdownParam;
|
||||
import run.halo.app.model.support.HaloConst;
|
||||
import run.halo.app.model.vo.PostMarkdownVO;
|
||||
import run.halo.app.security.service.OneTimeTokenService;
|
||||
import run.halo.app.service.*;
|
||||
import run.halo.app.utils.DateTimeUtils;
|
||||
|
@ -45,12 +48,14 @@ import java.time.LocalDateTime;
|
|||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Backup service implementation.
|
||||
*
|
||||
* @author johnniang
|
||||
* @author ryanwang
|
||||
* @author Raremaa
|
||||
* @date 2019-04-26
|
||||
*/
|
||||
@Service
|
||||
|
@ -59,10 +64,14 @@ public class BackupServiceImpl implements BackupService {
|
|||
|
||||
private static final String BACKUP_RESOURCE_BASE_URI = "/api/admin/backups/work-dir";
|
||||
|
||||
private static final String DATA_EXPORT_MARKDOWN_BASE_URI = "/api/admin/backups/markdown/export";
|
||||
|
||||
private static final String DATA_EXPORT_BASE_URI = "/api/admin/backups/data";
|
||||
|
||||
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
|
||||
|
||||
private final static String UPLOAD_SUB_DIR = "upload/";
|
||||
|
||||
private static final Type MAP_TYPE = new TypeToken<Map<String, ?>>() {
|
||||
}.getType();
|
||||
|
||||
|
@ -429,6 +438,121 @@ public class BackupServiceImpl implements BackupService {
|
|||
eventPublisher.publishEvent(new ThemeUpdatedEvent(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BackupDTO exportMarkdowns(PostMarkdownParam postMarkdownParam) throws IOException {
|
||||
// Query all Post data
|
||||
List<PostMarkdownVO> postMarkdownList = postService.listPostMarkdowns();
|
||||
Assert.notEmpty(postMarkdownList, "当前无文章可以导出");
|
||||
|
||||
// Write files to the temporary directory
|
||||
String markdownFileTempPathName = haloProperties.getBackupMarkdownDir() + IdUtil.simpleUUID().hashCode();
|
||||
for (int i = 0; i < postMarkdownList.size(); i++) {
|
||||
PostMarkdownVO postMarkdownVO = postMarkdownList.get(i);
|
||||
StringBuilder content = new StringBuilder();
|
||||
Boolean needFrontMatter = Optional.ofNullable(postMarkdownParam.getNeedFrontMatter()).orElse(false);
|
||||
if (needFrontMatter) {
|
||||
// Add front-matter
|
||||
content.append(postMarkdownVO.getFrontMatter()).append("\n");
|
||||
}
|
||||
content.append(postMarkdownVO.getOriginalContent());
|
||||
try {
|
||||
String markdownFileName = postMarkdownVO.getTitle() + "-" + postMarkdownVO.getSlug() + ".md";
|
||||
Path markdownFilePath = Paths.get(markdownFileTempPathName, markdownFileName);
|
||||
if (!Files.exists(markdownFilePath.getParent())) {
|
||||
Files.createDirectories(markdownFilePath.getParent());
|
||||
}
|
||||
Path markdownDataPath = Files.createFile(markdownFilePath);
|
||||
FileWriter fileWriter = new FileWriter(markdownDataPath.toFile(), CharsetUtil.UTF_8);
|
||||
fileWriter.write(content.toString());
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("导出数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
ZipOutputStream markdownZipOut = null;
|
||||
// Zip file
|
||||
try {
|
||||
// Create zip path
|
||||
String markdownZipFileName = HaloConst.HALO_BACKUP_MARKDOWN_PREFIX +
|
||||
DateTimeUtils.format(LocalDateTime.now(), DateTimeUtils.HORIZONTAL_LINE_DATETIME_FORMATTER) +
|
||||
IdUtil.simpleUUID().hashCode() + ".zip";
|
||||
|
||||
// Create zip file
|
||||
Path markdownZipFilePath = Paths.get(haloProperties.getBackupMarkdownDir(), markdownZipFileName);
|
||||
if (!Files.exists(markdownZipFilePath.getParent())) {
|
||||
Files.createDirectories(markdownZipFilePath.getParent());
|
||||
}
|
||||
Path markdownZipPath = Files.createFile(markdownZipFilePath);
|
||||
|
||||
markdownZipOut = new ZipOutputStream(Files.newOutputStream(markdownZipPath));
|
||||
|
||||
// Zip temporary directory
|
||||
Path markdownFileTempPath = Paths.get(markdownFileTempPathName);
|
||||
run.halo.app.utils.FileUtils.zip(markdownFileTempPath, markdownZipOut);
|
||||
|
||||
// Zip upload sub-directory
|
||||
String uploadPathName = FileHandler.normalizeDirectory(haloProperties.getWorkDir()) + UPLOAD_SUB_DIR;
|
||||
Path uploadPath = Paths.get(uploadPathName);
|
||||
if (Files.exists(uploadPath)) {
|
||||
run.halo.app.utils.FileUtils.zip(uploadPath, markdownZipOut);
|
||||
}
|
||||
|
||||
// Remove files in the temporary directory
|
||||
run.halo.app.utils.FileUtils.deleteFolder(markdownFileTempPath);
|
||||
|
||||
// Build backup dto
|
||||
return buildBackupDto(DATA_EXPORT_MARKDOWN_BASE_URI, markdownZipPath);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to export markdowns", e);
|
||||
} finally {
|
||||
if (markdownZipOut != null) {
|
||||
markdownZipOut.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BackupDTO> listMarkdowns() {
|
||||
// Ensure the parent folder exist
|
||||
Path backupParentPath = Paths.get(haloProperties.getBackupMarkdownDir());
|
||||
if (Files.notExists(backupParentPath)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Build backup dto
|
||||
try (Stream<Path> subPathStream = Files.list(backupParentPath)) {
|
||||
return subPathStream
|
||||
.filter(backupPath -> StringUtils.startsWithIgnoreCase(backupPath.getFileName().toString(), HaloConst.HALO_BACKUP_MARKDOWN_PREFIX))
|
||||
.map(backupPath -> buildBackupDto(DATA_EXPORT_MARKDOWN_BASE_URI, backupPath))
|
||||
.sorted(Comparator.comparingLong(BackupDTO::getUpdateTime).reversed())
|
||||
.collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to fetch backups", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMarkdown(String fileName) {
|
||||
Assert.hasText(fileName, "File name must not be blank");
|
||||
|
||||
Path backupRootPath = Paths.get(haloProperties.getBackupMarkdownDir());
|
||||
|
||||
// Get backup path
|
||||
Path backupPath = backupRootPath.resolve(fileName);
|
||||
|
||||
// Check directory traversal
|
||||
run.halo.app.utils.FileUtils.checkDirectoryTraversal(backupRootPath, backupPath);
|
||||
|
||||
try {
|
||||
// Delete backup file
|
||||
Files.delete(backupPath);
|
||||
} catch (NoSuchFileException e) {
|
||||
throw new NotFoundException("The file " + fileName + " was not found", e);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to delete backup", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds backup dto.
|
||||
*
|
||||
|
|
|
@ -27,10 +27,7 @@ import run.halo.app.model.enums.PostStatus;
|
|||
import run.halo.app.model.params.PostParam;
|
||||
import run.halo.app.model.params.PostQuery;
|
||||
import run.halo.app.model.properties.PostProperties;
|
||||
import run.halo.app.model.vo.ArchiveMonthVO;
|
||||
import run.halo.app.model.vo.ArchiveYearVO;
|
||||
import run.halo.app.model.vo.PostDetailVO;
|
||||
import run.halo.app.model.vo.PostListVO;
|
||||
import run.halo.app.model.vo.*;
|
||||
import run.halo.app.repository.PostRepository;
|
||||
import run.halo.app.repository.base.BasePostRepository;
|
||||
import run.halo.app.service.*;
|
||||
|
@ -57,6 +54,7 @@ import static run.halo.app.model.support.HaloConst.URL_SEPARATOR;
|
|||
* @author guqing
|
||||
* @author evanwang
|
||||
* @author coor.top
|
||||
* @author Raremaa
|
||||
* @date 2019-03-14
|
||||
*/
|
||||
@Slf4j
|
||||
|
@ -819,6 +817,64 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
return Sort.by(DESC, "topPriority").and(Sort.by(DESC, indexSort).and(Sort.by(DESC, "id")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PostMarkdownVO> listPostMarkdowns() {
|
||||
List<Post> allPostList = listAll();
|
||||
List<PostMarkdownVO> result = new ArrayList(allPostList.size());
|
||||
for (int i = 0; i < allPostList.size(); i++) {
|
||||
Post post = allPostList.get(i);
|
||||
result.add(convertToPostMarkdownVo(post));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private PostMarkdownVO convertToPostMarkdownVo(Post post) {
|
||||
PostMarkdownVO postMarkdownVO = new PostMarkdownVO();
|
||||
|
||||
StringBuilder frontMatter = new StringBuilder("---\n");
|
||||
frontMatter.append("title: ").append(post.getTitle()).append("\n");
|
||||
frontMatter.append("date: ").append(post.getCreateTime()).append("\n");
|
||||
frontMatter.append("updated: ").append(post.getUpdateTime()).append("\n");
|
||||
|
||||
//set fullPath
|
||||
frontMatter.append("url: ").append(buildFullPath(post)).append("\n");
|
||||
|
||||
//set category
|
||||
List<Category> categories = postCategoryService.listCategoriesBy(post.getId());
|
||||
StringBuilder categoryContent = new StringBuilder();
|
||||
for (int i = 0; i < categories.size(); i++) {
|
||||
Category category = categories.get(i);
|
||||
String categoryName = category.getName();
|
||||
if (i == 0) {
|
||||
categoryContent.append(categoryName);
|
||||
} else {
|
||||
categoryContent.append(" | ").append(categoryName);
|
||||
}
|
||||
}
|
||||
frontMatter.append("categories: ").append(categoryContent.toString()).append("\n");
|
||||
|
||||
//set tags
|
||||
List<Tag> tags = postTagService.listTagsBy(post.getId());
|
||||
StringBuilder tagContent = new StringBuilder();
|
||||
for (int i = 0; i < tags.size(); i++) {
|
||||
Tag tag = tags.get(i);
|
||||
String tagName = tag.getName();
|
||||
if (i == 0) {
|
||||
tagContent.append(tagName);
|
||||
} else {
|
||||
tagContent.append(" | ").append(tagName);
|
||||
}
|
||||
}
|
||||
frontMatter.append("tags: ").append(tagContent.toString()).append("\n");
|
||||
|
||||
frontMatter.append("---\n");
|
||||
postMarkdownVO.setFrontMatter(frontMatter.toString());
|
||||
postMarkdownVO.setOriginalContent(post.getOriginalContent());
|
||||
postMarkdownVO.setTitle(post.getTitle());
|
||||
postMarkdownVO.setSlug(post.getSlug());
|
||||
return postMarkdownVO;
|
||||
}
|
||||
|
||||
private String buildFullPath(Post post) {
|
||||
|
||||
PostPermalinkType permalinkType = optionService.getPostPermalinkType();
|
||||
|
|
|
@ -36,7 +36,7 @@ class PostServiceImplTest {
|
|||
|
||||
@Test
|
||||
void getContent() {
|
||||
String exportMarkdown = postService.exportMarkdown(18);
|
||||
String exportMarkdown = postService.exportMarkdown(1);
|
||||
log.debug(exportMarkdown);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue