Provide backup dto fetch api (#1278)

* Fix swagger security reference config error

* Add backup dto fetch api

* Rearrange fetch api
pull/1279/head
John Niang 2021-02-19 23:26:17 +08:00 committed by GitHub
parent 49a461f245
commit b57712e23e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 82 deletions

View File

@ -146,8 +146,8 @@ public class SwaggerConfiguration {
private List<SecurityScheme> 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<SecurityScheme> 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<SecurityReference> defaultAuth() {
private List<SecurityReference> 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<SecurityReference> contentApiAuth() {
private List<SecurityReference> 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() {

View File

@ -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<Resource> downloadBackup(@PathVariable("fileName") String fileName,
public ResponseEntity<Resource> 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

View File

@ -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

View File

@ -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);
}
}

View File

@ -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<BackupDTO> 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<BackupDTO> 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;
}
}
}

View File

@ -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<Map<String, ?>>() {
}.getType();
private static final Type JSON_OBJECT_TYPE = new TypeToken<List<JsonObject>>() {
}.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<BackupDTO> 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<HashMap<String, Object>> typeRef =
new TypeReference<HashMap<String, Object>>() {
new TypeReference<>() {
};
HashMap<String, Object> 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);
}