Accomplish backup listing and deletion

pull/389/head
johnniang 2019-11-18 02:37:36 +08:00
parent a041409ff7
commit c7d88b1d1f
12 changed files with 243 additions and 30 deletions

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import freemarker.template.TemplateException; import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler; import freemarker.template.TemplateExceptionHandler;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.boot.jackson.JsonComponentModule;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
@ -80,16 +81,17 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer {
*/ */
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
String workDir = FILE_PROTOCOL + haloProperties.getWorkDir(); String workDir = FILE_PROTOCOL + StringUtils.appendIfMissing(haloProperties.getWorkDir(), "/");
String backupDir = FILE_PROTOCOL + StringUtils.appendIfMissing(haloProperties.getBackupDir(), "/");
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations(workDir + "templates/themes/") .addResourceLocations(workDir + "templates/themes/")
.addResourceLocations(workDir + "templates/admin/") .addResourceLocations(workDir + "templates/admin/")
.addResourceLocations("classpath:/admin/") .addResourceLocations("classpath:/admin/")
.addResourceLocations(workDir + "static/"); .addResourceLocations(workDir + "static/");
registry.addResourceHandler("/upload/**") registry.addResourceHandler(haloProperties.getUploadUrlPrefix() + "/**")
.addResourceLocations(workDir + "upload/"); .addResourceLocations(workDir + "upload/");
registry.addResourceHandler("/backup/**") registry.addResourceHandler(haloProperties.getBackupUrlPrefix() + "/**")
.addResourceLocations(workDir + "backup/"); .addResourceLocations(workDir + "backup/", backupDir);
registry.addResourceHandler(haloProperties.getAdminPath() + "/**") registry.addResourceHandler(haloProperties.getAdminPath() + "/**")
.addResourceLocations(workDir + HALO_ADMIN_RELATIVE_PATH) .addResourceLocations(workDir + HALO_ADMIN_RELATIVE_PATH)
.addResourceLocations("classpath:/admin/"); .addResourceLocations("classpath:/admin/");

View File

@ -4,10 +4,12 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import run.halo.app.model.support.HaloConst; import run.halo.app.model.support.HaloConst;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
/** /**
* Halo configuration properties. * Halo configuration properties.
* *
@ -37,13 +39,29 @@ public class HaloProperties {
*/ */
private String adminPath = "/admin"; private String adminPath = "/admin";
/**
* Halo backup directory.(Not recommended to modify this config);
*/
private String backupDir = HaloConst.TEMP_DIR + "/halo-backup/";
/** /**
* Work directory. * Work directory.
*/ */
private String workDir = HaloConst.USER_HOME + "/.halo/"; private String workDir = HaloConst.USER_HOME + "/.halo/";
/**
* Upload prefix.
*/
private String uploadUrlPrefix = "/upload";
/**
* backup prefix.
*/
private String backupUrlPrefix = "/backup";
public HaloProperties() throws IOException { public HaloProperties() throws IOException {
// Create work directory if not exist // Create work directory if not exist
Files.createDirectories(Paths.get(workDir)); Files.createDirectories(Paths.get(workDir));
Files.createDirectories(Paths.get(backupDir));
} }
} }

View File

@ -6,13 +6,10 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import org.json.JSONObject; import org.json.JSONObject;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
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 org.springframework.web.multipart.MultipartFile;
import run.halo.app.exception.FileOperationException; import run.halo.app.exception.FileOperationException;
import run.halo.app.model.dto.BackupDTO;
import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.model.dto.post.BasePostDetailDTO;
import run.halo.app.service.BackupService; import run.halo.app.service.BackupService;
@ -40,6 +37,24 @@ public class BackupController {
this.backupService = backupService; this.backupService = backupService;
} }
@PostMapping("halo")
@ApiOperation("Backup halo")
public BackupDTO backupHalo() {
return backupService.zipWorkDirectory();
}
@GetMapping("halo")
@ApiOperation("Get all backups")
public List<BackupDTO> listBackups() {
return backupService.listHaloBackups();
}
@DeleteMapping("halo")
@ApiOperation("Delete a backup")
public void deleteBackup(@RequestParam("filename") String filename) {
backupService.deleteHaloBackup(filename);
}
@PostMapping("import/markdown") @PostMapping("import/markdown")
@ApiOperation("Import markdown") @ApiOperation("Import markdown")
public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException { public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException {

View File

@ -37,10 +37,10 @@ public class MainController {
this.haloProperties = haloProperties; this.haloProperties = haloProperties;
} }
@GetMapping("/{permlink}") // @GetMapping("/{permlink}")
public String admin(@PathVariable(name = "permlink") String permlink) { // public String admin(@PathVariable(name = "permlink") String permlink) {
return "redirect:/" + permlink + "/index.html"; // return "redirect:/" + permlink + "/index.html";
} // }
@GetMapping("/install") @GetMapping("/install")
public String installation() { public String installation() {

View File

@ -11,13 +11,7 @@ import java.util.Date;
@Data @Data
public class BackupDTO { public class BackupDTO {
private String fileName; private String downloadUrl;
private Date createTime; private String filename;
private String fileSize;
private String fileType;
private String type;
} }

View File

@ -13,6 +13,7 @@ import java.util.Date;
* @date : 2018/6/4 * @date : 2018/6/4
*/ */
@Data @Data
@Deprecated
public class BackupDto { public class BackupDto {
/** /**

View File

@ -17,6 +17,11 @@ public class HaloConst {
*/ */
public final static String USER_HOME = System.getProperties().getProperty("user.home"); public final static String USER_HOME = System.getProperties().getProperty("user.home");
/**
* Temporary directory.
*/
public final static String TEMP_DIR = System.getProperties().getProperty("java.io.tmpdir");
/** /**
* Default theme name. * Default theme name.
*/ */

View File

@ -1,7 +1,9 @@
package run.halo.app.service; package run.halo.app.service;
import org.json.JSONObject; import org.json.JSONObject;
import org.springframework.lang.NonNull;
import org.springframework.web.multipart.MultipartFile; 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.dto.post.BasePostDetailDTO;
import java.io.IOException; import java.io.IOException;
@ -27,7 +29,7 @@ public interface BackupService {
/** /**
* export posts by hexo formatter * export posts by hexo formatter
* *
* @return * @return json object
*/ */
JSONObject exportHexoMDs(); JSONObject exportHexoMDs();
@ -38,4 +40,28 @@ public interface BackupService {
* @param path * @param path
*/ */
void exportHexoMd(List<JSONObject> posts, String path); void exportHexoMd(List<JSONObject> posts, String path);
/**
* Zips work directory.
*
* @return backup dto.
*/
@NonNull
BackupDTO zipWorkDirectory();
/**
* Lists all backups.
*
* @return backup list
*/
@NonNull
List<BackupDTO> listHaloBackups();
/**
* Deletes backup.
*
* @param filename filename must not be blank
*/
void deleteHaloBackup(@NonNull String filename);
} }

View File

@ -1,26 +1,39 @@
package run.halo.app.service.impl; package run.halo.app.service.impl;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import lombok.RequiredArgsConstructor; import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import org.json.JSONObject; import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.NotFoundException;
import run.halo.app.exception.ServiceException;
import run.halo.app.model.dto.AttachmentDTO;
import run.halo.app.model.dto.BackupDTO;
import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.model.dto.post.BasePostDetailDTO;
import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Post;
import run.halo.app.model.entity.Tag; import run.halo.app.model.entity.Tag;
import run.halo.app.service.BackupService; import run.halo.app.service.BackupService;
import run.halo.app.service.OptionService;
import run.halo.app.service.PostService; import run.halo.app.service.PostService;
import run.halo.app.service.PostTagService; import run.halo.app.service.PostTagService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -35,14 +48,27 @@ import java.util.stream.Collectors;
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class BackupServiceImpl implements BackupService { public class BackupServiceImpl implements BackupService {
private final PostService postService; private final PostService postService;
private final PostTagService postTagService; private final PostTagService postTagService;
public static final String LINE_SEPARATOR = System.getProperty("line.separator"); private final OptionService optionService;
private final HaloProperties halo;
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public BackupServiceImpl(PostService postService,
PostTagService postTagService,
OptionService optionService,
HaloProperties halo) {
this.postService = postService;
this.postTagService = postTagService;
this.optionService = optionService;
this.halo = halo;
}
@Override @Override
public BasePostDetailDTO importMarkdown(MultipartFile file) throws IOException { public BasePostDetailDTO importMarkdown(MultipartFile file) throws IOException {
@ -117,6 +143,74 @@ public class BackupServiceImpl implements BackupService {
}); });
} }
@Override
public BackupDTO zipWorkDirectory() {
// Zip work directory to temporary file
try {
// Create zip path for halo zip
String haloZipFileName = new StringBuilder().append("Halo-backup-")
.append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")))
.append(IdUtil.simpleUUID())
.append(".zip").toString();
// Create halo zip file
Path haloZipPath = Files.createFile(Paths.get(halo.getBackupDir(), haloZipFileName));
// Zip halo
run.halo.app.utils.FileUtils.zip(Paths.get(this.halo.getWorkDir()), haloZipPath);
// Build download url
String downloadUrl = buildDownloadUrl(haloZipFileName);
// Build attachment dto
BackupDTO backup = new BackupDTO();
backup.setDownloadUrl(downloadUrl);
backup.setFilename(haloZipFileName);
return backup;
} catch (IOException e) {
throw new ServiceException("Failed to backup halo", e);
}
}
@Override
public List<BackupDTO> listHaloBackups() {
try {
return Files.list(Paths.get(halo.getBackupDir())).map(backupPath -> {
// Get filename
String filename = backupPath.getFileName().toString();
// Build download url
String downloadUrl = buildDownloadUrl(filename);
// Build backup dto
BackupDTO backup = new BackupDTO();
backup.setDownloadUrl(downloadUrl);
backup.setFilename(filename);
return backup;
}).collect(Collectors.toList());
} catch (IOException e) {
throw new ServiceException("Failed to fetch backups", e);
}
}
@Override
public void deleteHaloBackup(String filename) {
Assert.hasText(filename, "File name must not be blank");
// Get backup path
Path backupPath = Paths.get(halo.getBackupDir(), filename);
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);
}
}
/** /**
* Sanitizes the specified file name. * Sanitizes the specified file name.
* *
@ -129,4 +223,19 @@ public class BackupServiceImpl implements BackupService {
replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~]", ""). replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~]", "").
replaceAll("\\s", ""); replaceAll("\\s", "");
} }
/**
* Builds download url.
*
* @param filename filename must not be blank
* @return download url
*/
private String buildDownloadUrl(@NonNull String filename) {
Assert.hasText(filename, "File name must not be blank");
return StringUtils.joinWith("/",
optionService.getBlogBaseUrl(),
StringUtils.removeEnd(StringUtils.removeStart(halo.getBackupUrlPrefix(), "/"), "/"),
filename);
}
} }

View File

@ -124,13 +124,26 @@ public class FileUtils {
/** /**
* Zip folder or file. * Zip folder or file.
* *
* @param fileToZip file path to zip must not be null * @param pathToZip file path to zip must not be null
* @param pathOfArchive zip file path to archive must not be null
* @throws IOException
*/
public static void zip(@NonNull Path pathToZip, @NonNull Path pathOfArchive) throws IOException {
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(pathOfArchive))) {
zip(pathToZip, zipOut);
}
}
/**
* Zip folder or file.
*
* @param pathToZip file path to zip must not be null
* @param zipOut zip output stream must not be null * @param zipOut zip output stream must not be null
* @throws IOException * @throws IOException
*/ */
public static void zip(@NonNull Path fileToZip, @NonNull ZipOutputStream zipOut) throws IOException { public static void zip(@NonNull Path pathToZip, @NonNull ZipOutputStream zipOut) throws IOException {
// Zip file // Zip file
zip(fileToZip, fileToZip.getFileName().toString(), zipOut); zip(pathToZip, pathToZip.getFileName().toString(), zipOut);
} }
/** /**

View File

@ -3,6 +3,7 @@ package run.halo.app.utils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import run.halo.app.model.support.HaloConst;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -82,10 +83,13 @@ public class FileUtilsTest {
FileUtils.zip(rootFolder, zipOut); FileUtils.zip(rootFolder, zipOut);
} }
// Clear the test folder created before // Clear the test folder created before
FileUtils.deleteFolder(rootFolder); FileUtils.deleteFolder(rootFolder);
Files.delete(zipToStore); Files.delete(zipToStore);
}
@Test
public void tempFolderTest() {
log.debug(HaloConst.TEMP_DIR);
} }
} }

View File

@ -0,0 +1,26 @@
package run.halo.app.utils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Local date time test.
*
* @author johnniang
*/
@Slf4j
public class LocalDateTimeTest {
@Test
public void dateTimeToStringTest() {
LocalDateTime dateTime = LocalDateTime.now();
log.debug(dateTime.toString());
log.debug(dateTime.toLocalDate().toString());
String DATE_FORMATTER = "yyyy-MM-dd-HH-mm-ss-";
log.debug(dateTime.format(DateTimeFormatter.ofPattern(DATE_FORMATTER)));
}
}