mirror of
https://github.com/blossom-editor/blossom.git
synced 2026-03-12 17:41:26 +08:00
fix: 下载备份文件时可以指定任意文件名的问题
This commit is contained in:
@@ -1,21 +1,19 @@
|
||||
package com.blossom.backend.server.article.backup;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.param.ParamEnum;
|
||||
import com.blossom.backend.base.param.ParamService;
|
||||
import com.blossom.backend.base.param.pojo.ParamEntity;
|
||||
import com.blossom.backend.server.article.backup.pojo.BackupFile;
|
||||
import com.blossom.backend.server.article.backup.pojo.DownloadReq;
|
||||
import com.blossom.backend.server.utils.DownloadUtil;
|
||||
import com.blossom.common.base.enums.YesNo;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import com.blossom.common.base.util.SortUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.PathResource;
|
||||
import org.springframework.core.io.support.ResourceRegion;
|
||||
import org.springframework.http.*;
|
||||
@@ -23,10 +21,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -36,6 +31,7 @@ import java.util.stream.Collectors;
|
||||
* @author xzzz
|
||||
* @order 8
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/article/backup")
|
||||
@@ -52,7 +48,7 @@ public class ArticleBackupController {
|
||||
* @param articleId 备份指定的文章
|
||||
*/
|
||||
@GetMapping
|
||||
public R<ArticleBackupService.BackupFile> backup(
|
||||
public R<BackupFile> backup(
|
||||
@RequestParam("type") String type,
|
||||
@RequestParam("toLocal") String toLocal,
|
||||
@RequestParam(value = "articleId", required = false) Long articleId) {
|
||||
@@ -77,7 +73,7 @@ public class ArticleBackupController {
|
||||
* 备份记录
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public R<List<ArticleBackupService.BackupFile>> list() {
|
||||
public R<List<BackupFile>> list() {
|
||||
return R.ok(backupService.listAll(AuthContext.getUserId())
|
||||
.stream()
|
||||
.sorted((b1, b2) -> SortUtil.dateSort.compare(b1.getDatetime(), b2.getDatetime()))
|
||||
@@ -89,9 +85,12 @@ public class ArticleBackupController {
|
||||
* 下载压缩包
|
||||
*
|
||||
* @param filename 文件名称
|
||||
* @deprecated 1.14.0
|
||||
*/
|
||||
@GetMapping("/download")
|
||||
@Deprecated
|
||||
public void download(@RequestParam("filename") String filename, HttpServletResponse response) {
|
||||
/*
|
||||
final ParamEntity param = paramService.getValue(ParamEnum.BACKUP_PATH);
|
||||
XzException500.throwBy(ObjUtil.isNull(param), ArticleBackupService.ERROR_MSG);
|
||||
final String rootPath = param.getParamValue();
|
||||
@@ -103,6 +102,7 @@ public class ArticleBackupController {
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +112,6 @@ public class ArticleBackupController {
|
||||
* @param request request
|
||||
* @apiNote 返回类 ResponseEntity<ResourceRegion>
|
||||
*/
|
||||
@AuthIgnore
|
||||
@GetMapping("/download/fragment")
|
||||
public ResponseEntity<ResourceRegion> downloadFragment(@RequestParam("filename") String filename,
|
||||
HttpServletRequest request) {
|
||||
@@ -129,15 +128,17 @@ public class ArticleBackupController {
|
||||
* @apiNote 返回类 ResponseEntity<ResourceRegion>
|
||||
* @apiNote 通过 Range 请求头获取分片请求, 返回头中会比说明本次分片大小 Content-Range
|
||||
*/
|
||||
@AuthIgnore
|
||||
@PostMapping("/download/fragment")
|
||||
public ResponseEntity<ResourceRegion> downloadFragment(@RequestBody DownloadReq req,
|
||||
HttpServletRequest request) {
|
||||
final ParamEntity param = paramService.getValue(ParamEnum.BACKUP_PATH);
|
||||
XzException500.throwBy(ObjUtil.isNull(param), ArticleBackupService.ERROR_MSG);
|
||||
final String rootPath = param.getParamValue();
|
||||
XzException500.throwBy(StrUtil.isBlank(rootPath), ArticleBackupService.ERROR_MSG);
|
||||
String filename = rootPath + "/" + req.getFilename();
|
||||
// 检查文件名
|
||||
if (!checkFilename(req.getFilename())) {
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.NOT_FOUND)
|
||||
.body(new ResourceRegion(new PathResource(""), 0, 0));
|
||||
}
|
||||
// 拼接文件名
|
||||
String filename = getRootPath() + "/" + req.getFilename();
|
||||
File file = new File(filename);
|
||||
long contentLength = file.length();
|
||||
|
||||
@@ -167,12 +168,43 @@ public class ArticleBackupController {
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.OK)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
|
||||
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(resourceRegion.getCount()))
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.header(HttpHeaders.CONTENT_RANGE, contentRange)
|
||||
.body(resourceRegion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份根目录
|
||||
*/
|
||||
private String getRootPath() {
|
||||
final ParamEntity param = paramService.getValue(ParamEnum.BACKUP_PATH);
|
||||
XzException500.throwBy(ObjUtil.isNull(param), ArticleBackupService.ERROR_MSG);
|
||||
final String rootPath = param.getParamValue();
|
||||
XzException500.throwBy(StrUtil.isBlank(rootPath), ArticleBackupService.ERROR_MSG);
|
||||
return rootPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化文件名, 不能包含 / 进行隐性的路径切换
|
||||
*/
|
||||
private boolean checkFilename(String filename) {
|
||||
// 不能包含 /
|
||||
if (filename.contains("/")) {
|
||||
return false;
|
||||
}
|
||||
if (filename.contains("%2f")) {
|
||||
return false;
|
||||
}
|
||||
if (filename.contains("%2F")) {
|
||||
return false;
|
||||
}
|
||||
BackupFile backupFile = new BackupFile();
|
||||
backupFile.build(filename);
|
||||
if (!backupFile.checkPrefix()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.blossom.backend.server.article.backup;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@@ -12,6 +11,7 @@ import com.blossom.backend.base.param.ParamService;
|
||||
import com.blossom.backend.base.param.pojo.ParamEntity;
|
||||
import com.blossom.backend.base.user.UserService;
|
||||
import com.blossom.backend.base.user.pojo.UserEntity;
|
||||
import com.blossom.backend.server.article.backup.pojo.BackupFile;
|
||||
import com.blossom.backend.server.article.draft.ArticleService;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.reference.ArticleReferenceService;
|
||||
@@ -29,8 +29,6 @@ import com.blossom.common.base.util.DateUtils;
|
||||
import com.blossom.common.base.util.PrimaryKeyUtil;
|
||||
import com.blossom.common.base.util.SortUtil;
|
||||
import com.blossom.common.iaas.IaasProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -72,7 +70,6 @@ public class ArticleBackupService {
|
||||
private Executor executor;
|
||||
|
||||
public static final String ERROR_MSG = String.format("[文章备份] 备份失败, 未配置备份路径 [%s]", ParamEnum.BACKUP_PATH.name());
|
||||
private static final String SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* 查看记录
|
||||
@@ -443,111 +440,4 @@ public class ArticleBackupService {
|
||||
.replaceAll("\\|", "")
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份文件
|
||||
*/
|
||||
@Data
|
||||
public static class BackupFile {
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
/**
|
||||
* 备份日期 YYYYMMDD
|
||||
*
|
||||
* @mock 20230101
|
||||
*/
|
||||
private String date;
|
||||
/**
|
||||
* 备份时间 HHMMSS
|
||||
*
|
||||
* @mock 123001
|
||||
*/
|
||||
private String time;
|
||||
/**
|
||||
* 备份的日期和时间, yyyy-MM-dd HH:mm:ss
|
||||
*/
|
||||
private Date datetime;
|
||||
/**
|
||||
* 备份包的名称
|
||||
*/
|
||||
private String filename;
|
||||
/**
|
||||
* 备份包路径
|
||||
*/
|
||||
private String path;
|
||||
/**
|
||||
* 本地文件
|
||||
*/
|
||||
@JsonIgnore
|
||||
private File file;
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private Long fileLength;
|
||||
|
||||
/**
|
||||
* 通过本地备份文件初始化
|
||||
*
|
||||
* @param file 本地备份文件
|
||||
*/
|
||||
public BackupFile(File file) {
|
||||
build(FileUtil.getPrefix(file.getName()));
|
||||
this.file = file;
|
||||
this.fileLength = file.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定用户的开始备份
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public BackupFile(Long userId, BackupTypeEnum type, YesNo toLocal) {
|
||||
String filename = String.format("%s_%s_%s", buildFilePrefix(type, toLocal), userId, DateUtils.toYMDHMS_SSS(System.currentTimeMillis()));
|
||||
filename = filename.replaceAll(" ", SEPARATOR)
|
||||
.replaceAll("-", "")
|
||||
.replaceAll(":", "")
|
||||
.replaceAll("\\.", SEPARATOR);
|
||||
build(filename);
|
||||
}
|
||||
|
||||
private static String buildFilePrefix(BackupTypeEnum type, YesNo toLocal) {
|
||||
String prefix = "B";
|
||||
if (type == BackupTypeEnum.MARKDOWN) {
|
||||
prefix += "M";
|
||||
} else if (type == BackupTypeEnum.HTML) {
|
||||
prefix += "H";
|
||||
}
|
||||
|
||||
if (toLocal == YesNo.YES) {
|
||||
prefix += "L";
|
||||
} else if (toLocal == YesNo.NO) {
|
||||
prefix += "N";
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
private void build(String filename) {
|
||||
this.filename = filename;
|
||||
String[] tags = filename.split(SEPARATOR);
|
||||
if (tags.length < 5) {
|
||||
return;
|
||||
}
|
||||
this.userId = tags[1];
|
||||
this.date = tags[2];
|
||||
this.time = tags[3];
|
||||
this.datetime = DateUtil.parse(this.date + this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份文件的路径, 由备份路径 + 本次备份名称构成
|
||||
*/
|
||||
public String getRootPath() {
|
||||
return this.path + "/" + this.filename;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.blossom.backend.server.article.backup.pojo;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import com.blossom.backend.server.article.backup.BackupTypeEnum;
|
||||
import com.blossom.common.base.enums.YesNo;
|
||||
import com.blossom.common.base.util.DateUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
public class BackupFile {
|
||||
|
||||
private static final String SEPARATOR = "_";
|
||||
private static Set<String> prefixs = new HashSet<String>() {{
|
||||
this.add("BML");
|
||||
this.add("BMN");
|
||||
this.add("BHL");
|
||||
this.add("BHN");
|
||||
}};
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
/**
|
||||
* 备份日期 YYYYMMDD
|
||||
*
|
||||
* @mock 20230101
|
||||
*/
|
||||
private String date;
|
||||
/**
|
||||
* 备份时间 HHMMSS
|
||||
*
|
||||
* @mock 123001
|
||||
*/
|
||||
private String time;
|
||||
/**
|
||||
* 备份的日期和时间, yyyy-MM-dd HH:mm:ss
|
||||
*/
|
||||
private Date datetime;
|
||||
/**
|
||||
* 备份包的名称
|
||||
*/
|
||||
private String filename;
|
||||
/**
|
||||
* 备份包路径
|
||||
*/
|
||||
private String path;
|
||||
/**
|
||||
* 本地文件
|
||||
*/
|
||||
@JsonIgnore
|
||||
private File file;
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private Long fileLength;
|
||||
|
||||
/**
|
||||
* 通过本地备份文件初始化
|
||||
*
|
||||
* @param file 本地备份文件
|
||||
*/
|
||||
public BackupFile(File file) {
|
||||
build(FileUtil.getPrefix(file.getName()));
|
||||
this.file = file;
|
||||
this.fileLength = file.length();
|
||||
}
|
||||
|
||||
public BackupFile() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定用户的开始备份
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public BackupFile(Long userId, BackupTypeEnum type, YesNo toLocal) {
|
||||
String filename = String.format("%s_%s_%s", buildFilePrefix(type, toLocal), userId, DateUtils.toYMDHMS_SSS(System.currentTimeMillis()));
|
||||
filename = filename.replaceAll(" ", SEPARATOR)
|
||||
.replaceAll("-", "")
|
||||
.replaceAll(":", "")
|
||||
.replaceAll("\\.", SEPARATOR);
|
||||
build(filename);
|
||||
}
|
||||
|
||||
private static String buildFilePrefix(BackupTypeEnum type, YesNo toLocal) {
|
||||
String prefix = "B";
|
||||
if (type == BackupTypeEnum.MARKDOWN) {
|
||||
prefix += "M";
|
||||
} else if (type == BackupTypeEnum.HTML) {
|
||||
prefix += "H";
|
||||
}
|
||||
|
||||
if (toLocal == YesNo.YES) {
|
||||
prefix += "L";
|
||||
} else if (toLocal == YesNo.NO) {
|
||||
prefix += "N";
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public void build(String filename) {
|
||||
this.filename = filename;
|
||||
String[] tags = filename.split(SEPARATOR);
|
||||
if (tags.length < 5) {
|
||||
return;
|
||||
}
|
||||
this.userId = tags[1];
|
||||
this.date = tags[2];
|
||||
this.time = tags[3];
|
||||
this.datetime = DateUtil.parse(this.date + this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件格式
|
||||
*/
|
||||
public boolean checkPrefix() {
|
||||
String[] tags = filename.split(SEPARATOR);
|
||||
if (tags.length != 5) {
|
||||
return false;
|
||||
}
|
||||
String prefix = tags[0];
|
||||
// 不是固定文件前缀则失败
|
||||
if (!prefixs.contains(prefix)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取备份文件的路径, 由备份路径 + 本次备份名称构成
|
||||
*/
|
||||
public String getRootPath() {
|
||||
return this.path + "/" + this.filename;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user