diff --git a/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java b/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java index f728b51a8..1f2294f3a 100644 --- a/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java +++ b/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -80,16 +81,17 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { */ @Override 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("/**") .addResourceLocations(workDir + "templates/themes/") .addResourceLocations(workDir + "templates/admin/") .addResourceLocations("classpath:/admin/") .addResourceLocations(workDir + "static/"); - registry.addResourceHandler("/upload/**") + registry.addResourceHandler(haloProperties.getUploadUrlPrefix() + "/**") .addResourceLocations(workDir + "upload/"); - registry.addResourceHandler("/backup/**") - .addResourceLocations(workDir + "backup/"); + registry.addResourceHandler(haloProperties.getBackupUrlPrefix() + "/**") + .addResourceLocations(workDir + "backup/", backupDir); registry.addResourceHandler(haloProperties.getAdminPath() + "/**") .addResourceLocations(workDir + HALO_ADMIN_RELATIVE_PATH) .addResourceLocations("classpath:/admin/"); diff --git a/src/main/java/run/halo/app/config/properties/HaloProperties.java b/src/main/java/run/halo/app/config/properties/HaloProperties.java index 52714c23b..148bb7128 100644 --- a/src/main/java/run/halo/app/config/properties/HaloProperties.java +++ b/src/main/java/run/halo/app/config/properties/HaloProperties.java @@ -4,10 +4,12 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import run.halo.app.model.support.HaloConst; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; + /** * Halo configuration properties. * @@ -37,13 +39,29 @@ public class HaloProperties { */ private String adminPath = "/admin"; + /** + * Halo backup directory.(Not recommended to modify this config); + */ + private String backupDir = HaloConst.TEMP_DIR + "/halo-backup/"; + /** * Work directory. */ private String workDir = HaloConst.USER_HOME + "/.halo/"; + /** + * Upload prefix. + */ + private String uploadUrlPrefix = "/upload"; + + /** + * backup prefix. + */ + private String backupUrlPrefix = "/backup"; + public HaloProperties() throws IOException { // Create work directory if not exist Files.createDirectories(Paths.get(workDir)); + Files.createDirectories(Paths.get(backupDir)); } } 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 5cbd2fabb..2643f0868 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 @@ -6,13 +6,10 @@ 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.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; 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.service.BackupService; @@ -40,6 +37,24 @@ public class BackupController { this.backupService = backupService; } + @PostMapping("halo") + @ApiOperation("Backup halo") + public BackupDTO backupHalo() { + return backupService.zipWorkDirectory(); + } + + @GetMapping("halo") + @ApiOperation("Get all backups") + public List listBackups() { + return backupService.listHaloBackups(); + } + + @DeleteMapping("halo") + @ApiOperation("Delete a backup") + public void deleteBackup(@RequestParam("filename") String filename) { + backupService.deleteHaloBackup(filename); + } + @PostMapping("import/markdown") @ApiOperation("Import markdown") public BasePostDetailDTO backupMarkdowns(@RequestPart("file") MultipartFile file) throws IOException { diff --git a/src/main/java/run/halo/app/controller/content/MainController.java b/src/main/java/run/halo/app/controller/content/MainController.java index d8beab087..b5274d0d0 100644 --- a/src/main/java/run/halo/app/controller/content/MainController.java +++ b/src/main/java/run/halo/app/controller/content/MainController.java @@ -37,10 +37,10 @@ public class MainController { this.haloProperties = haloProperties; } - @GetMapping("/{permlink}") - public String admin(@PathVariable(name = "permlink") String permlink) { - return "redirect:/" + permlink + "/index.html"; - } +// @GetMapping("/{permlink}") +// public String admin(@PathVariable(name = "permlink") String permlink) { +// return "redirect:/" + permlink + "/index.html"; +// } @GetMapping("/install") public String installation() { diff --git a/src/main/java/run/halo/app/model/dto/BackupDTO.java b/src/main/java/run/halo/app/model/dto/BackupDTO.java index ca37da6dd..26684ec1d 100644 --- a/src/main/java/run/halo/app/model/dto/BackupDTO.java +++ b/src/main/java/run/halo/app/model/dto/BackupDTO.java @@ -11,13 +11,7 @@ import java.util.Date; @Data public class BackupDTO { - private String fileName; + private String downloadUrl; - private Date createTime; - - private String fileSize; - - private String fileType; - - private String type; + private String filename; } diff --git a/src/main/java/run/halo/app/model/support/BackupDto.java b/src/main/java/run/halo/app/model/support/BackupDto.java index b9bcea8e3..9b0d37dce 100644 --- a/src/main/java/run/halo/app/model/support/BackupDto.java +++ b/src/main/java/run/halo/app/model/support/BackupDto.java @@ -13,6 +13,7 @@ import java.util.Date; * @date : 2018/6/4 */ @Data +@Deprecated public class BackupDto { /** diff --git a/src/main/java/run/halo/app/model/support/HaloConst.java b/src/main/java/run/halo/app/model/support/HaloConst.java index b86a3cdf5..9cc03c22a 100644 --- a/src/main/java/run/halo/app/model/support/HaloConst.java +++ b/src/main/java/run/halo/app/model/support/HaloConst.java @@ -17,6 +17,11 @@ public class HaloConst { */ 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. */ diff --git a/src/main/java/run/halo/app/service/BackupService.java b/src/main/java/run/halo/app/service/BackupService.java index c17a69f0c..6a4061b9a 100644 --- a/src/main/java/run/halo/app/service/BackupService.java +++ b/src/main/java/run/halo/app/service/BackupService.java @@ -1,7 +1,9 @@ package run.halo.app.service; import org.json.JSONObject; +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 java.io.IOException; @@ -27,7 +29,7 @@ public interface BackupService { /** * export posts by hexo formatter * - * @return + * @return json object */ JSONObject exportHexoMDs(); @@ -38,4 +40,28 @@ public interface BackupService { * @param path */ void exportHexoMd(List posts, String path); + + /** + * Zips work directory. + * + * @return backup dto. + */ + @NonNull + BackupDTO zipWorkDirectory(); + + + /** + * Lists all backups. + * + * @return backup list + */ + @NonNull + List listHaloBackups(); + + /** + * Deletes backup. + * + * @param filename filename must not be blank + */ + void deleteHaloBackup(@NonNull String filename); } 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 71e50187f..a8d60e79c 100644 --- a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java @@ -1,26 +1,39 @@ package run.halo.app.service.impl; import cn.hutool.core.io.IoUtil; -import lombok.RequiredArgsConstructor; +import cn.hutool.core.util.IdUtil; 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.lang.NonNull; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import org.springframework.web.multipart.MultipartFile; 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.entity.Post; import run.halo.app.model.entity.Tag; import run.halo.app.service.BackupService; +import run.halo.app.service.OptionService; 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.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.LinkedHashMap; import java.util.List; @@ -35,14 +48,27 @@ import java.util.stream.Collectors; */ @Service @Slf4j -@RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class BackupServiceImpl implements BackupService { private final PostService postService; 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 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 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. * @@ -129,4 +223,19 @@ public class BackupServiceImpl implements BackupService { replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~]", ""). 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); + } } diff --git a/src/main/java/run/halo/app/utils/FileUtils.java b/src/main/java/run/halo/app/utils/FileUtils.java index a2ef421e2..195f57a6c 100644 --- a/src/main/java/run/halo/app/utils/FileUtils.java +++ b/src/main/java/run/halo/app/utils/FileUtils.java @@ -124,13 +124,26 @@ public class FileUtils { /** * 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 * @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(fileToZip, fileToZip.getFileName().toString(), zipOut); + zip(pathToZip, pathToZip.getFileName().toString(), zipOut); } /** diff --git a/src/test/java/run/halo/app/utils/FileUtilsTest.java b/src/test/java/run/halo/app/utils/FileUtilsTest.java index f618e6248..868419b6d 100644 --- a/src/test/java/run/halo/app/utils/FileUtilsTest.java +++ b/src/test/java/run/halo/app/utils/FileUtilsTest.java @@ -3,6 +3,7 @@ package run.halo.app.utils; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; +import run.halo.app.model.support.HaloConst; import java.io.File; import java.io.IOException; @@ -82,10 +83,13 @@ public class FileUtilsTest { FileUtils.zip(rootFolder, zipOut); } - // Clear the test folder created before FileUtils.deleteFolder(rootFolder); Files.delete(zipToStore); + } + @Test + public void tempFolderTest() { + log.debug(HaloConst.TEMP_DIR); } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/utils/LocalDateTimeTest.java b/src/test/java/run/halo/app/utils/LocalDateTimeTest.java new file mode 100644 index 000000000..71c216d54 --- /dev/null +++ b/src/test/java/run/halo/app/utils/LocalDateTimeTest.java @@ -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))); + } + +}