From b57712e23eba75167b72b46b6e091ac2bace15e1 Mon Sep 17 00:00:00 2001 From: John Niang Date: Fri, 19 Feb 2021 23:26:17 +0800 Subject: [PATCH] Provide backup dto fetch api (#1278) * Fix swagger security reference config error * Add backup dto fetch api * Rearrange fetch api --- .../halo/app/config/SwaggerConfiguration.java | 24 ++--- .../admin/api/BackupController.java | 37 ++++++- .../filter/AbstractAuthenticationFilter.java | 3 +- .../service/impl/OneTimeTokenServiceImpl.java | 17 ++-- .../run/halo/app/service/BackupService.java | 34 +++++++ .../app/service/impl/BackupServiceImpl.java | 96 ++++++++----------- 6 files changed, 129 insertions(+), 82 deletions(-) diff --git a/src/main/java/run/halo/app/config/SwaggerConfiguration.java b/src/main/java/run/halo/app/config/SwaggerConfiguration.java index e4fbc9ad6..42b21ba31 100644 --- a/src/main/java/run/halo/app/config/SwaggerConfiguration.java +++ b/src/main/java/run/halo/app/config/SwaggerConfiguration.java @@ -146,8 +146,8 @@ public class SwaggerConfiguration { private List adminApiKeys() { return Arrays.asList( - new ApiKey("Token from header", ADMIN_TOKEN_HEADER_NAME, In.HEADER.name()), - new ApiKey("Token from query", ADMIN_TOKEN_QUERY_NAME, In.QUERY.name()) + new ApiKey(ADMIN_TOKEN_HEADER_NAME, ADMIN_TOKEN_HEADER_NAME, In.HEADER.name()), + new ApiKey(ADMIN_TOKEN_QUERY_NAME, ADMIN_TOKEN_QUERY_NAME, In.QUERY.name()) ); } @@ -155,7 +155,7 @@ public class SwaggerConfiguration { final PathMatcher pathMatcher = new AntPathMatcher(); return Collections.singletonList( SecurityContext.builder() - .securityReferences(defaultAuth()) + .securityReferences(adminApiAuths()) .operationSelector(operationContext -> { var requestMappingPattern = operationContext.requestMappingPattern(); return pathMatcher.match("/api/admin/**/*", requestMappingPattern); @@ -166,8 +166,8 @@ public class SwaggerConfiguration { private List contentApiKeys() { return Arrays.asList( - new ApiKey("Access key from header", API_ACCESS_KEY_HEADER_NAME, In.HEADER.name()), - new ApiKey("Access key from query", API_ACCESS_KEY_QUERY_NAME, In.QUERY.name()) + new ApiKey(API_ACCESS_KEY_HEADER_NAME, API_ACCESS_KEY_HEADER_NAME, In.HEADER.name()), + new ApiKey(API_ACCESS_KEY_QUERY_NAME, API_ACCESS_KEY_QUERY_NAME, In.QUERY.name()) ); } @@ -175,7 +175,7 @@ public class SwaggerConfiguration { final PathMatcher pathMatcher = new AntPathMatcher(); return Collections.singletonList( SecurityContext.builder() - .securityReferences(contentApiAuth()) + .securityReferences(contentApiAuths()) .operationSelector(operationContext -> { var requestMappingPattern = operationContext.requestMappingPattern(); return pathMatcher.match("/api/content/**/*", requestMappingPattern); @@ -184,18 +184,18 @@ public class SwaggerConfiguration { ); } - private List defaultAuth() { + private List adminApiAuths() { AuthorizationScope[] authorizationScopes = {new AuthorizationScope("Admin api", "Access admin api")}; - return Arrays.asList(new SecurityReference("Token from header", authorizationScopes), - new SecurityReference("Token from query", authorizationScopes)); + return Arrays.asList(new SecurityReference(ADMIN_TOKEN_HEADER_NAME, authorizationScopes), + new SecurityReference(ADMIN_TOKEN_QUERY_NAME, authorizationScopes)); } - private List contentApiAuth() { + private List contentApiAuths() { AuthorizationScope[] authorizationScopes = {new AuthorizationScope("content api", "Access content api")}; - return Arrays.asList(new SecurityReference("Access key from header", authorizationScopes), - new SecurityReference("Access key from query", authorizationScopes)); + return Arrays.asList(new SecurityReference(API_ACCESS_KEY_HEADER_NAME, authorizationScopes), + new SecurityReference(API_ACCESS_KEY_QUERY_NAME, authorizationScopes)); } private ApiInfo apiInfo() { 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 7e0321aff..0fc63512d 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 @@ -1,7 +1,12 @@ package run.halo.app.controller.admin.api; +import static run.halo.app.service.BackupService.BackupType.JSON_DATA; +import static run.halo.app.service.BackupService.BackupType.MARKDOWN; +import static run.halo.app.service.BackupService.BackupType.WHOLE_SITE; + import io.swagger.annotations.ApiOperation; import java.io.IOException; +import java.nio.file.Paths; import java.util.List; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; @@ -21,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import run.halo.app.annotation.DisableOnCondition; import run.halo.app.config.properties.HaloProperties; +import run.halo.app.exception.NotFoundException; import run.halo.app.model.dto.BackupDTO; import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.model.params.PostMarkdownParam; @@ -48,6 +54,29 @@ public class BackupController { this.haloProperties = haloProperties; } + @GetMapping("work-dir/fetch") + public BackupDTO getWorkDirBackup(@RequestParam("filename") String filename) { + return backupService.getBackup(Paths.get(haloProperties.getWorkDir(), filename), WHOLE_SITE) + .orElseThrow(() -> + new NotFoundException("备份文件 " + filename + " 不存在或已删除!").setErrorData(filename)); + } + + @GetMapping("data/fetch") + public BackupDTO getDataBackup(@RequestParam("filename") String filename) { + return backupService + .getBackup(Paths.get(haloProperties.getDataExportDir(), filename), JSON_DATA) + .orElseThrow(() -> + new NotFoundException("备份文件 " + filename + " 不存在或已删除!").setErrorData(filename)); + } + + @GetMapping("markdown/fetch") + public BackupDTO getMarkdownBackup(@RequestParam("filename") String filename) { + return backupService + .getBackup(Paths.get(haloProperties.getBackupMarkdownDir(), filename), MARKDOWN) + .orElseThrow(() -> + new NotFoundException("备份文件 " + filename + " 不存在或已删除!").setErrorData(filename)); + } + @PostMapping("work-dir") @ApiOperation("Backups work directory") @DisableOnCondition @@ -61,16 +90,16 @@ public class BackupController { return backupService.listWorkDirBackups(); } - @GetMapping("work-dir/{fileName:.+}") + @GetMapping("work-dir/{filename:.+}") @ApiOperation("Downloads a work directory backup file") @DisableOnCondition - public ResponseEntity downloadBackup(@PathVariable("fileName") String fileName, + public ResponseEntity downloadBackup(@PathVariable("filename") String filename, HttpServletRequest request) { - log.info("Try to download backup file: [{}]", fileName); + log.info("Trying to download backup file: [{}]", filename); // Load file as resource Resource backupResource = - backupService.loadFileAsResource(haloProperties.getBackupDir(), fileName); + backupService.loadFileAsResource(haloProperties.getBackupDir(), filename); String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; // Try to determine file's content type diff --git a/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java b/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java index a10210cde..06c0922eb 100644 --- a/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java +++ b/src/main/java/run/halo/app/security/filter/AbstractAuthenticationFilter.java @@ -252,7 +252,8 @@ public abstract class AbstractAuthenticationFilter extends OncePerRequestFilter // Get allowed uri String allowedUri = oneTimeTokenService.get(oneTimeToken) - .orElseThrow(() -> new BadRequestException("The one-time token does not exist") + .orElseThrow(() -> new BadRequestException( + "The one-time token does not exist or has been expired") .setErrorData(oneTimeToken)); // Get request uri diff --git a/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java b/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java index 26afcaf8e..42ea29ce6 100644 --- a/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java +++ b/src/main/java/run/halo/app/security/service/impl/OneTimeTokenServiceImpl.java @@ -1,5 +1,6 @@ package run.halo.app.security.service.impl; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.springframework.stereotype.Service; @@ -16,10 +17,9 @@ import run.halo.app.utils.HaloUtils; @Service public class OneTimeTokenServiceImpl implements OneTimeTokenService { - /** - * One-time token expired day. (unit: day) - */ - public static final int OTT_EXPIRED_DAY = 1; + private static final String tokenPrefix = "OTT-"; + + private static final Duration OTT_EXPIRATION_TIME = Duration.ofMinutes(5); private final AbstractStringCacheStore cacheStore; @@ -32,7 +32,7 @@ public class OneTimeTokenServiceImpl implements OneTimeTokenService { Assert.hasText(oneTimeToken, "One-time token must not be blank"); // Get from cache store - return cacheStore.get(oneTimeToken); + return cacheStore.get(tokenPrefix + oneTimeToken); } @Override @@ -43,7 +43,10 @@ public class OneTimeTokenServiceImpl implements OneTimeTokenService { String oneTimeToken = HaloUtils.randomUUIDWithoutDash(); // Put ott along with request uri - cacheStore.put(oneTimeToken, uri, OTT_EXPIRED_DAY, TimeUnit.DAYS); + cacheStore.put(tokenPrefix + oneTimeToken, + uri, + OTT_EXPIRATION_TIME.getSeconds(), + TimeUnit.SECONDS); // Return ott return oneTimeToken; @@ -54,6 +57,6 @@ public class OneTimeTokenServiceImpl implements OneTimeTokenService { Assert.hasText(oneTimeToken, "One-time token must not be blank"); // Delete the token - cacheStore.delete(oneTimeToken); + cacheStore.delete(tokenPrefix + oneTimeToken); } } diff --git a/src/main/java/run/halo/app/service/BackupService.java b/src/main/java/run/halo/app/service/BackupService.java index 531d905eb..6ee5ef640 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 java.io.IOException; +import java.nio.file.Path; import java.util.List; +import java.util.Optional; import org.springframework.core.io.Resource; import org.springframework.lang.NonNull; import org.springframework.web.multipart.MultipartFile; @@ -44,6 +46,16 @@ public interface BackupService { @NonNull List listWorkDirBackups(); + /** + * Get backup data by backup file name. + * + * @param backupFileName backup file name must not be blank + * @param type backup type must not be null + * @return an optional of backup data + */ + @NonNull + Optional getBackup(@NonNull Path backupFileName, @NonNull BackupType type); + /** * Deletes backup. * @@ -116,4 +128,26 @@ public interface BackupService { * @param fileName file name */ void deleteMarkdown(@NonNull String fileName); + + /** + * Backup type. + * + * @author johnniang + */ + enum BackupType { + WHOLE_SITE("/api/admin/backups/work-dir"), + JSON_DATA("/api/admin/backups/data"), + MARKDOWN("/api/admin/backups/markdown/export"), + ; + + private final String baseUri; + + BackupType(String baseUri) { + this.baseUri = baseUri; + } + + public String getBaseUri() { + return baseUri; + } + } } 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 fe7027ad9..47e0ed272 100644 --- a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java @@ -4,6 +4,7 @@ import static run.halo.app.model.support.HaloConst.HALO_BACKUP_MARKDOWN_PREFIX; import static run.halo.app.model.support.HaloConst.HALO_BACKUP_PREFIX; import static run.halo.app.model.support.HaloConst.HALO_DATA_EXPORT_PREFIX; import static run.halo.app.utils.DateTimeUtils.HORIZONTAL_LINE_DATETIME_FORMATTER; +import static run.halo.app.utils.FileUtils.checkDirectoryTraversal; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.IoUtil; @@ -12,10 +13,7 @@ import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.IdUtil; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; import java.io.IOException; -import java.lang.reflect.Type; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -119,16 +117,8 @@ public class BackupServiceImpl implements BackupService { private static final String DATA_EXPORT_BASE_URI = "/api/admin/backups/data"; - private static final String LINE_SEPARATOR = System.getProperty("line.separator"); - private static final String UPLOAD_SUB_DIR = "upload/"; - private static final Type MAP_TYPE = new TypeToken>() { - }.getType(); - - private static final Type JSON_OBJECT_TYPE = new TypeToken>() { - }.getType(); - private final AttachmentService attachmentService; private final CategoryService categoryService; @@ -214,18 +204,6 @@ public class BackupServiceImpl implements BackupService { this.eventPublisher = eventPublisher; } - /** - * Sanitizes the specified file name. - * - * @param unSanitized the specified file name - * @return sanitized file name - */ - public static String sanitizeFilename(final String unSanitized) { - return unSanitized.replaceAll("[^(a-zA-Z0-9\\u4e00-\\u9fa5\\.)]", "") - .replaceAll("[\\?\\\\/:|<>\\*\\[\\]\\(\\)\\$%\\{\\}@~\\.]", "") - .replaceAll("\\s", ""); - } - @Override public BasePostDetailDTO importMarkdown(MultipartFile file) throws IOException { @@ -233,7 +211,6 @@ public class BackupServiceImpl implements BackupService { String markdown = IoUtil.read(file.getInputStream(), StandardCharsets.UTF_8); // TODO sheet import - return postService.importMarkdown(markdown, file.getOriginalFilename()); } @@ -285,6 +262,16 @@ public class BackupServiceImpl implements BackupService { } } + @Override + public Optional getBackup(@NonNull Path backupFilePath, @NonNull BackupType type) { + if (Files.notExists(backupFilePath)) { + return Optional.empty(); + } + + BackupDTO backupDto = buildBackupDto(type.getBaseUri(), backupFilePath); + return Optional.of(backupDto); + } + @Override public void deleteWorkDirBackup(String fileName) { Assert.hasText(fileName, "File name must not be blank"); @@ -295,7 +282,7 @@ public class BackupServiceImpl implements BackupService { Path backupPath = backupRootPath.resolve(fileName); // Check directory traversal - run.halo.app.utils.FileUtils.checkDirectoryTraversal(backupRootPath, backupPath); + checkDirectoryTraversal(backupRootPath, backupPath); try { // Delete backup file @@ -324,7 +311,7 @@ public class BackupServiceImpl implements BackupService { Path backupFilePath = Paths.get(basePath, fileName).normalize(); // Check directory traversal - run.halo.app.utils.FileUtils.checkDirectoryTraversal(backupParentPath, backupFilePath); + checkDirectoryTraversal(backupParentPath, backupFilePath); // Build url resource Resource backupResource = new UrlResource(backupFilePath.toUri()); @@ -418,7 +405,7 @@ public class BackupServiceImpl implements BackupService { Path backupPath = dataExportRootPath.resolve(fileName); - run.halo.app.utils.FileUtils.checkDirectoryTraversal(dataExportRootPath, backupPath); + checkDirectoryTraversal(dataExportRootPath, backupPath); try { // Delete backup file @@ -436,7 +423,7 @@ public class BackupServiceImpl implements BackupService { ObjectMapper mapper = JsonUtils.createDefaultJsonMapper(); TypeReference> typeRef = - new TypeReference>() { + new TypeReference<>() { }; HashMap data = mapper.readValue(jsonContent, typeRef); @@ -539,19 +526,18 @@ public class BackupServiceImpl implements BackupService { // 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); + for (PostMarkdownVO postMarkdownVo : postMarkdownList) { 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.getFrontMatter()).append("\n"); } - content.append(postMarkdownVO.getOriginalContent()); + content.append(postMarkdownVo.getOriginalContent()); try { String markdownFileName = - postMarkdownVO.getTitle() + "-" + postMarkdownVO.getSlug() + ".md"; + postMarkdownVo.getTitle() + "-" + postMarkdownVo.getSlug() + ".md"; Path markdownFilePath = Paths.get(markdownFileTempPathName, markdownFileName); if (!Files.exists(markdownFilePath.getParent())) { Files.createDirectories(markdownFilePath.getParent()); @@ -565,23 +551,21 @@ public class BackupServiceImpl implements BackupService { } } - ZipOutputStream markdownZipOut = null; + // Create zip path + String markdownZipFileName = HALO_BACKUP_MARKDOWN_PREFIX + + DateTimeUtils.format(LocalDateTime.now(), 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); // Zip file - try { - // Create zip path - String markdownZipFileName = HALO_BACKUP_MARKDOWN_PREFIX - + DateTimeUtils.format(LocalDateTime.now(), 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)); + try (ZipOutputStream markdownZipOut = new ZipOutputStream( + Files.newOutputStream(markdownZipPath))) { // Zip temporary directory Path markdownFileTempPath = Paths.get(markdownFileTempPathName); @@ -602,10 +586,6 @@ public class BackupServiceImpl implements BackupService { 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(); - } } } @@ -632,22 +612,22 @@ public class BackupServiceImpl implements BackupService { } @Override - public void deleteMarkdown(String fileName) { - Assert.hasText(fileName, "File name must not be blank"); + 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); + Path backupPath = backupRootPath.resolve(filename); // Check directory traversal - run.halo.app.utils.FileUtils.checkDirectoryTraversal(backupRootPath, backupPath); + checkDirectoryTraversal(backupRootPath, backupPath); try { // Delete backup file Files.delete(backupPath); } catch (NoSuchFileException e) { - throw new NotFoundException("The file " + fileName + " was not found", e); + throw new NotFoundException("The file " + filename + " was not found", e); } catch (IOException e) { throw new ServiceException("Failed to delete backup", e); }