Skip to content

Commit

Permalink
feat: add api for markdown export (halo-dev#1199)
Browse files Browse the repository at this point in the history
* add API for markdown-export

* add front-matter support

* optimize fileName for markdown-export

* fornt-matter与正文中间增加换行符
  • Loading branch information
Raremaa authored Dec 27, 2020
1 parent 2fb5d14 commit 7c72fdb
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand All @@ -23,6 +24,7 @@
*
* @author johnniang
* @author ryanwang
* @author Raremaa
* @date 2019-04-26
*/
@RestController
Expand All @@ -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;
}
Expand Down Expand Up @@ -84,7 +85,7 @@ public void deleteBackup(@RequestParam("filename") String filename) {
backupService.deleteWorkDirBackup(filename);
}

@PostMapping("markdown")
@PostMapping("markdown/import")
@ApiOperation("Imports markdown")
public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException {
return backupService.importMarkdown(file);
Expand Down Expand Up @@ -132,4 +133,50 @@ public ResponseEntity<Resource> downloadExportedData(@PathVariable("fileName") S
.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);
}


}
17 changes: 17 additions & 0 deletions src/main/java/run/halo/app/model/params/PostMarkdownParam.java
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions src/main/java/run/halo/app/model/support/HaloConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/run/halo/app/model/vo/PostMarkdownVO.java
Original file line number Diff line number Diff line change
@@ -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;
}
26 changes: 26 additions & 0 deletions src/main/java/run/halo/app/service/BackupService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand Down Expand Up @@ -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);
}
17 changes: 11 additions & 6 deletions src/main/java/run/halo/app/service/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
124 changes: 124 additions & 0 deletions src/main/java/run/halo/app/service/impl/BackupServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
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;
Expand All @@ -45,12 +48,14 @@
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
Expand All @@ -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();

Expand Down Expand Up @@ -429,6 +438,121 @@ public void importData(MultipartFile file) throws IOException {
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.
*
Expand Down
Loading

0 comments on commit 7c72fdb

Please sign in to comment.