fix: 下载备份文件时可以指定任意文件名的问题

This commit is contained in:
xiaozzzi
2024-03-19 16:18:08 +08:00
parent 5296055032
commit 682a5c8533
3 changed files with 195 additions and 128 deletions

View File

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

View File

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

View File

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