refactor: backup service. (#687)

* refactor: backup service.

* fix: test case error.
pull/689/head
Ryan Wang 2020-03-17 20:22:51 +08:00 committed by GitHub
parent 03a43fb245
commit a77ebed299
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 50 deletions

View File

@ -14,6 +14,8 @@ import static run.halo.app.utils.HaloUtils.ensureSuffix;
* Halo configuration properties.
*
* @author johnniang
* @author ryanwang
* @date 2019-03-15
*/
@Data
@ConfigurationProperties("halo")
@ -54,6 +56,11 @@ public class HaloProperties {
*/
private String backupDir = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "halo-backup" + FILE_SEPARATOR;
/**
* Halo data export directory.
*/
private String dataExportDir = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "halo-data-export" + FILE_SEPARATOR;
/**
* Upload prefix.
*/

View File

@ -1,6 +1,5 @@
package run.halo.app.controller.admin.api;
import cn.hutool.core.date.DateUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
@ -9,6 +8,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.model.annotation.DisableOnCondition;
import run.halo.app.model.dto.BackupDTO;
import run.halo.app.model.dto.post.BasePostDetailDTO;
@ -16,13 +16,13 @@ import run.halo.app.service.BackupService;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Date;
import java.util.List;
/**
* Backup controller
*
* @author johnniang
* @author ryanwang
* @date 2019-04-26
*/
@RestController
@ -32,31 +32,35 @@ public class BackupController {
private final BackupService backupService;
public BackupController(BackupService backupService) {
private final HaloProperties haloProperties;
public BackupController(BackupService backupService,
HaloProperties haloProperties) {
this.backupService = backupService;
this.haloProperties = haloProperties;
}
@PostMapping("halo")
@ApiOperation("Backups halo")
@PostMapping("work-dir")
@ApiOperation("Backups work directory")
@DisableOnCondition
public BackupDTO backupHalo() {
return backupService.zipWorkDirectory();
return backupService.backupWorkDirectory();
}
@GetMapping("halo")
@ApiOperation("Gets all backups")
@GetMapping("work-dir")
@ApiOperation("Gets all work directory backups")
public List<BackupDTO> listBackups() {
return backupService.listHaloBackups();
return backupService.listWorkDirBackups();
}
@GetMapping("halo/{fileName:.+}")
@ApiOperation("Downloads backup file")
@GetMapping("work-dir/{fileName:.+}")
@ApiOperation("Downloads a work directory backup file")
@DisableOnCondition
public ResponseEntity<Resource> downloadBackup(@PathVariable("fileName") String fileName, HttpServletRequest request) {
log.info("Try to download backup file: [{}]", fileName);
// Load file as resource
Resource backupResource = backupService.loadFileAsResource(fileName);
Resource backupResource = backupService.loadFileAsResource(haloProperties.getBackupDir(), fileName);
String contentType = "application/octet-stream";
// Try to determine file's content type
@ -73,30 +77,59 @@ public class BackupController {
.body(backupResource);
}
@DeleteMapping("halo")
@ApiOperation("Deletes a backup")
@DeleteMapping("work-dir")
@ApiOperation("Deletes a work directory backup")
@DisableOnCondition
public void deleteBackup(@RequestParam("filename") String filename) {
backupService.deleteHaloBackup(filename);
backupService.deleteWorkDirBackup(filename);
}
@PostMapping("import/markdown")
@ApiOperation("Import markdown")
@PostMapping("markdown")
@ApiOperation("Imports markdown")
public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException {
return backupService.importMarkdown(file);
}
@GetMapping("export/data")
@PostMapping("data")
@ApiOperation("Exports all data")
@DisableOnCondition
public ResponseEntity<String> exportData() {
public BackupDTO exportData() {
return backupService.exportData();
}
String contentType = "application/octet-stream;charset=UTF-8";
@GetMapping("data")
@ApiOperation("Lists all exported data")
public List<BackupDTO> listExportedData() {
return backupService.listExportedData();
}
String filename = "halo-data-" + DateUtil.format(new Date(), "yyyy-MM-dd-HH-mm-ss.json");
@DeleteMapping("data")
@ApiOperation("Deletes a exported data")
@DisableOnCondition
public void deleteExportedData(@RequestParam("filename") String filename) {
backupService.deleteExportedData(filename);
}
@GetMapping("data/{fileName:.+}")
@ApiOperation("Downloads a exported data")
@DisableOnCondition
public ResponseEntity<Resource> downloadExportedData(@PathVariable("fileName") String fileName, HttpServletRequest request) {
log.info("Try to download exported data file: [{}]", fileName);
// Load exported data as resource
Resource exportDataResource = backupService.loadFileAsResource(haloProperties.getDataExportDir(), fileName);
String contentType = "application/octet-stream";
// Try to determine file's content type
try {
contentType = request.getServletContext().getMimeType(exportDataResource.getFile().getAbsolutePath());
} catch (IOException e) {
log.warn("Could not determine file type", e);
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.body(backupService.exportData().toJSONString());
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + exportDataResource.getFilename() + "\"")
.body(exportDataResource);
}
}

View File

@ -1,7 +1,9 @@
package run.halo.app.controller.admin.api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import run.halo.app.service.DataProcessService;
import run.halo.app.service.ThemeSettingService;

View File

@ -148,6 +148,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
private void initDirectory() {
Path workPath = Paths.get(haloProperties.getWorkDir());
Path backupPath = Paths.get(haloProperties.getBackupDir());
Path dataExportPath = Paths.get(haloProperties.getDataExportDir());
try {
if (Files.notExists(workPath)) {
@ -160,6 +161,11 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
log.info("Created backup directory: [{}]", backupPath);
}
if (Files.notExists(dataExportPath)) {
Files.createDirectories(dataExportPath);
log.info("Created data export directory: [{}]", dataExportPath);
}
} catch (IOException ie) {
throw new RuntimeException("Failed to initialize directories", ie);
}

View File

@ -9,9 +9,6 @@ import lombok.Data;
@Data
public class BackupDTO {
@Deprecated
private String downloadUrl;
private String downloadLink;
private String filename;

View File

@ -15,7 +15,7 @@ import java.util.Objects;
@Entity
@Table(name = "post_categories",
indexes = {@Index(name = "post_categories_post_id", columnList = "post_id"),
@Index(name = "post_categories_category_id", columnList = "category_id")})
@Index(name = "post_categories_category_id", columnList = "category_id")})
@Data
@ToString(callSuper = true)
public class PostCategory extends BaseEntity {

View File

@ -29,6 +29,11 @@ public class HaloConst {
*/
public final static String HALO_BACKUP_PREFIX = "halo-backup-";
/**
* Halo data export prefix.
*/
public final static String HALO_DATA_EXPORT_PREFIX = "halo-data-export-";
/**
* Static pages pack prefix.
*/

View File

@ -1,6 +1,5 @@
package run.halo.app.service;
import com.alibaba.fastjson.JSONObject;
import org.springframework.core.io.Resource;
import org.springframework.lang.NonNull;
import org.springframework.web.multipart.MultipartFile;
@ -14,6 +13,7 @@ import java.util.List;
* Backup service interface.
*
* @author johnniang
* @author ryanwang
* @date 2019-04-26
*/
public interface BackupService {
@ -33,7 +33,7 @@ public interface BackupService {
* @return backup dto.
*/
@NonNull
BackupDTO zipWorkDirectory();
BackupDTO backupWorkDirectory();
/**
@ -42,23 +42,24 @@ public interface BackupService {
* @return backup list
*/
@NonNull
List<BackupDTO> listHaloBackups();
List<BackupDTO> listWorkDirBackups();
/**
* Deletes backup.
*
* @param fileName filename must not be blank
*/
void deleteHaloBackup(@NonNull String fileName);
void deleteWorkDirBackup(@NonNull String fileName);
/**
* Loads file as resource.
*
* @param fileName backup file name must not be blank.
* @param basePath base path
* @return resource of the given file
*/
@NonNull
Resource loadFileAsResource(@NonNull String fileName);
Resource loadFileAsResource(@NonNull String basePath, @NonNull String fileName);
/**
@ -67,7 +68,21 @@ public interface BackupService {
* @return data
*/
@NonNull
JSONObject exportData();
BackupDTO exportData();
/**
* List all exported data.
*
* @return list of backup dto
*/
List<BackupDTO> listExportedData();
/**
* Deletes exported data.
*
* @param fileName fileName
*/
void deleteExportedData(@NonNull String fileName);
/**
* Import data

View File

@ -2,6 +2,8 @@ package run.halo.app.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
@ -51,7 +53,9 @@ import java.util.stream.Stream;
@Slf4j
public class BackupServiceImpl implements BackupService {
private static final String BACKUP_RESOURCE_BASE_URI = "/api/admin/backups/halo";
private static final String BACKUP_RESOURCE_BASE_URI = "/api/admin/backups/work-dir";
private static final String DATA_EXPORT_BASE_URI = "/api/admin/backups/data";
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
@ -155,7 +159,7 @@ public class BackupServiceImpl implements BackupService {
}
@Override
public BackupDTO zipWorkDirectory() {
public BackupDTO backupWorkDirectory() {
// Zip work directory to temporary file
try {
// Create zip path for halo zip
@ -169,14 +173,14 @@ public class BackupServiceImpl implements BackupService {
run.halo.app.utils.FileUtils.zip(Paths.get(this.haloProperties.getWorkDir()), haloZipPath);
// Build backup dto
return buildBackupDto(haloZipPath);
return buildBackupDto(BACKUP_RESOURCE_BASE_URI, haloZipPath);
} catch (IOException e) {
throw new ServiceException("Failed to backup halo", e);
}
}
@Override
public List<BackupDTO> listHaloBackups() {
public List<BackupDTO> listWorkDirBackups() {
// Ensure the parent folder exist
Path backupParentPath = Paths.get(haloProperties.getBackupDir());
if (Files.notExists(backupParentPath)) {
@ -187,7 +191,7 @@ public class BackupServiceImpl implements BackupService {
try (Stream<Path> subPathStream = Files.list(backupParentPath)) {
return subPathStream
.filter(backupPath -> StringUtils.startsWithIgnoreCase(backupPath.getFileName().toString(), HaloConst.HALO_BACKUP_PREFIX))
.map(this::buildBackupDto)
.map(backupPath -> buildBackupDto(BACKUP_RESOURCE_BASE_URI, backupPath))
.sorted((leftBackup, rightBackup) -> {
// Sort the result
if (leftBackup.getUpdateTime() < rightBackup.getUpdateTime()) {
@ -203,7 +207,7 @@ public class BackupServiceImpl implements BackupService {
}
@Override
public void deleteHaloBackup(String fileName) {
public void deleteWorkDirBackup(String fileName) {
Assert.hasText(fileName, "File name must not be blank");
Path backupRootPath = Paths.get(haloProperties.getBackupDir());
@ -225,10 +229,11 @@ public class BackupServiceImpl implements BackupService {
}
@Override
public Resource loadFileAsResource(String fileName) {
public Resource loadFileAsResource(String basePath, String fileName) {
Assert.hasText(basePath, "Base path must not be blank");
Assert.hasText(fileName, "Backup file name must not be blank");
Path backupParentPath = Paths.get(haloProperties.getBackupDir());
Path backupParentPath = Paths.get(basePath);
try {
if (Files.notExists(backupParentPath)) {
@ -237,7 +242,7 @@ public class BackupServiceImpl implements BackupService {
}
// Get backup file path
Path backupFilePath = Paths.get(haloProperties.getBackupDir(), fileName).normalize();
Path backupFilePath = Paths.get(basePath, fileName).normalize();
// Check directory traversal
run.halo.app.utils.FileUtils.checkDirectoryTraversal(backupParentPath, backupFilePath);
@ -258,7 +263,7 @@ public class BackupServiceImpl implements BackupService {
}
@Override
public JSONObject exportData() {
public BackupDTO exportData() {
JSONObject data = new JSONObject();
data.put("version", HaloConst.HALO_VERSION);
data.put("export_date", DateUtil.now());
@ -283,7 +288,67 @@ public class BackupServiceImpl implements BackupService {
data.put("tags", tagService.listAll());
data.put("theme_settings", themeSettingService.listAll());
data.put("user", userService.listAll());
return data;
try {
String haloDataFileName = HaloConst.HALO_DATA_EXPORT_PREFIX +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-")) +
IdUtil.simpleUUID().hashCode() + ".json";
Path haloDataPath = Files.createFile(Paths.get(haloProperties.getDataExportDir(), haloDataFileName));
FileWriter fileWriter = new FileWriter(haloDataPath.toFile(), CharsetUtil.UTF_8);
fileWriter.write(data.toJSONString());
return buildBackupDto(DATA_EXPORT_BASE_URI, haloDataPath);
} catch (IOException e) {
throw new ServiceException("导出数据失败", e);
}
}
@Override
public List<BackupDTO> listExportedData() {
Path exportedDataParentPath = Paths.get(haloProperties.getDataExportDir());
if (Files.notExists(exportedDataParentPath)) {
return Collections.emptyList();
}
try (Stream<Path> subPathStream = Files.list(exportedDataParentPath)) {
return subPathStream
.filter(backupPath -> StringUtils.startsWithIgnoreCase(backupPath.getFileName().toString(), HaloConst.HALO_DATA_EXPORT_PREFIX))
.map(backupPath -> buildBackupDto(DATA_EXPORT_BASE_URI, backupPath))
.sorted((leftBackup, rightBackup) -> {
// Sort the result
if (leftBackup.getUpdateTime() < rightBackup.getUpdateTime()) {
return 1;
} else if (leftBackup.getUpdateTime() > rightBackup.getUpdateTime()) {
return -1;
}
return 0;
}).collect(Collectors.toList());
} catch (IOException e) {
throw new ServiceException("Failed to fetch exported data", e);
}
}
@Override
public void deleteExportedData(String fileName) {
Assert.hasText(fileName, "File name must not be blank");
Path dataExportRootPath = Paths.get(haloProperties.getDataExportDir());
Path backupPath = dataExportRootPath.resolve(fileName);
run.halo.app.utils.FileUtils.checkDirectoryTraversal(dataExportRootPath, 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);
}
}
@Override
@ -363,14 +428,14 @@ public class BackupServiceImpl implements BackupService {
* @param backupPath backup path must not be null
* @return backup dto
*/
private BackupDTO buildBackupDto(@NonNull Path backupPath) {
private BackupDTO buildBackupDto(@NonNull String basePath, @NonNull Path backupPath) {
Assert.notNull(basePath, "Base path must not be null");
Assert.notNull(backupPath, "Backup path must not be null");
String backupFileName = backupPath.getFileName().toString();
BackupDTO backup = new BackupDTO();
try {
backup.setDownloadUrl(buildDownloadUrl(backupFileName));
backup.setDownloadLink(backup.getDownloadUrl());
backup.setDownloadLink(buildDownloadUrl(basePath, backupFileName));
backup.setFilename(backupFileName);
backup.setUpdateTime(Files.getLastModifiedTime(backupPath).toMillis());
backup.setFileSize(Files.size(backupPath));
@ -388,11 +453,12 @@ public class BackupServiceImpl implements BackupService {
* @return download url
*/
@NonNull
private String buildDownloadUrl(@NonNull String filename) {
private String buildDownloadUrl(@NonNull String basePath, @NonNull String filename) {
Assert.notNull(basePath, "Base path must not be null");
Assert.hasText(filename, "File name must not be blank");
// Composite http url
String backupUri = BACKUP_RESOURCE_BASE_URI + HaloUtils.URL_SEPARATOR + filename;
String backupUri = basePath + HaloUtils.URL_SEPARATOR + filename;
// Get a one-time token
String oneTimeToken = oneTimeTokenService.create(backupUri);