Skip to content

Commit 170f24a

Browse files
committed
feat: dir files backup and restore
1 parent fc26439 commit 170f24a

5 files changed

Lines changed: 256 additions & 11 deletions

File tree

blog-business-boot/src/main/java/com/hackyle/blog/business/controller/SystemManageController.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,87 @@ public ApiResponse<String> databaseRestore(@RequestParam(value = "restoreSql", r
113113
}
114114
}
115115

116+
/**
117+
* 文件夹的备份
118+
*/
119+
@GetMapping("/dirBackup")
120+
public void dirBackup(@RequestParam("fileDirPath") String fileDirPath, HttpServletResponse response){
121+
try {
122+
response.setContentType("application/json;charset=UTF-8");
123+
ServletOutputStream outputStream = response.getOutputStream();
124+
125+
if(StringUtils.isBlank(fileDirPath)) {
126+
String respStr = JSON.toJSONString(ApiResponse.error(ResponseEnum.PARAMETER_MISSING.getCode(), ResponseEnum.PARAMETER_MISSING.getMessage()));
127+
outputStream.write(respStr.getBytes(StandardCharsets.UTF_8));
128+
response.flushBuffer();
129+
outputStream.close();
130+
return;
131+
}
132+
133+
File fileDir = new File(fileDirPath);
134+
if(!fileDir.exists() || fileDir.isFile()) { //待备份的一定是一个图片文件夹,而非一个文件
135+
String respStr = JSON.toJSONString(ApiResponse.error(ResponseEnum.FRONT_END_ERROR.getCode(), ResponseEnum.FRONT_END_ERROR.getMessage(), "图片文件目录输入错误"));
136+
outputStream.write(respStr.getBytes(StandardCharsets.UTF_8));
137+
response.flushBuffer();
138+
outputStream.close();
139+
return;
140+
}
141+
142+
File databaseBackupFilePath = systemManageService.dirBackup(fileDir);
143+
if(databaseBackupFilePath == null) {
144+
String respStr = JSON.toJSONString(ApiResponse.error(ResponseEnum.OP_FAIL.getCode(), ResponseEnum.OP_FAIL.getMessage(), "文件夹备份失败!"));
145+
outputStream.write(respStr.getBytes(StandardCharsets.UTF_8));
146+
response.flushBuffer();
147+
outputStream.close();
148+
return;
149+
}
150+
151+
FileInputStream fis = new FileInputStream(databaseBackupFilePath);
152+
response.setContentType("application/octet-stream;charset=UTF-8"); //重新设置要传输的文件类型
153+
154+
//因为是跨前后端分离,默认reponse header只能取到以下:Content-Language,Content-Type,Expires,Last-Modified,Pragma
155+
//要想获取到文件名,需要采取这种方式。Reference:https://www.cnblogs.com/liuxianbin/p/13035809.html
156+
response.setHeader("filename", URLEncoder.encode(databaseBackupFilePath.getName(), StandardCharsets.UTF_8));
157+
response.setHeader("Access-Control-Expose-Headers","filename");
158+
response.setHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(databaseBackupFilePath.getName(), StandardCharsets.UTF_8));
159+
outputStream.write(fis.readAllBytes());
160+
outputStream.flush();
161+
outputStream.close();
162+
fis.close();
163+
164+
databaseBackupFilePath.delete(); //删除临时文件
165+
166+
} catch (Exception e) {
167+
LOGGER.error("文件夹备份时出现异常:", e);
168+
}
169+
}
170+
171+
/**
172+
* 文件夹的恢复
173+
* @param restoreDir 恢复到那个文件夹里
174+
*/
175+
@PostMapping(value = "/dirRestore")
176+
public ApiResponse<String> dirRestore(@RequestParam(value = "restoreFileZip", required = true) MultipartFile[] multipartFiles,
177+
@RequestParam(value = "restoreDir", required = true) String restoreDir) {
178+
if(multipartFiles == null || multipartFiles.length < 1) {
179+
return ApiResponse.error(ResponseEnum.FRONT_END_ERROR.getCode(), ResponseEnum.FRONT_END_ERROR.getMessage());
180+
}
181+
182+
//目前只支持恢复.tar.zip文件
183+
for (MultipartFile multipartFile : multipartFiles) {
184+
String fileName = multipartFile.getOriginalFilename();
185+
if((StringUtils.isNotBlank(fileName) && !fileName.contains(".tar.zip"))) {
186+
return ApiResponse.error(ResponseEnum.FRONT_END_ERROR.getCode(), ResponseEnum.FRONT_END_ERROR.getMessage(), "目前只支持从zip文件恢复");
187+
}
188+
}
189+
190+
try {
191+
systemManageService.dirRestore(multipartFiles, restoreDir);
192+
return ApiResponse.success(ResponseEnum.OP_OK.getCode(), ResponseEnum.OP_OK.getMessage());
193+
} catch (Exception e) {
194+
LOGGER.error("文件夹恢复时出现异常:", e);
195+
return ApiResponse.error(ResponseEnum.EXCEPTION.getCode(), ResponseEnum.EXCEPTION.getMessage());
196+
}
197+
}
198+
116199
}

blog-business-boot/src/main/java/com/hackyle/blog/business/service/SystemManageService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ public interface SystemManageService {
1313

1414
void databaseRestore(MultipartFile[] multipartFiles) throws IOException ;
1515

16+
File dirBackup(File fileDir) throws Exception;
17+
18+
void dirRestore(MultipartFile[] multipartFiles, String restoreDir) throws Exception;
1619
}

blog-business-boot/src/main/java/com/hackyle/blog/business/service/impl/SystemManageServiceImpl.java

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.hackyle.blog.business.util.FileCompressUtils;
99
import com.hackyle.blog.business.util.FileHandleUtils;
1010
import com.hackyle.blog.business.util.IpUtils;
11+
import org.apache.commons.lang3.StringUtils;
1112
import org.slf4j.Logger;
1213
import org.slf4j.LoggerFactory;
1314
import org.springframework.beans.factory.annotation.Value;
@@ -25,10 +26,7 @@
2526
import oshi.util.FormatUtil;
2627
import oshi.util.Util;
2728

28-
import java.io.File;
29-
import java.io.FileInputStream;
30-
import java.io.IOException;
31-
import java.io.InputStream;
29+
import java.io.*;
3230
import java.lang.management.ManagementFactory;
3331
import java.nio.file.Files;
3432
import java.text.DecimalFormat;
@@ -149,6 +147,73 @@ public void databaseRestore(MultipartFile[] multipartFiles) throws IOException {
149147
}
150148

151149

150+
151+
/**
152+
* 文件夹备份
153+
* @param fileDir 要备份那个目录
154+
*/
155+
@Override
156+
public File dirBackup(File fileDir) throws Exception{
157+
String osName = System.getProperty("os.name");
158+
if(osName.toLowerCase().startsWith(OperationTypeEnum.WIN.getName())) {
159+
throw new RuntimeException("The Windows Can't Be Backup!");
160+
}
161+
162+
String parentDir = fileDir.getParent();
163+
String dirName = fileDir.getName();
164+
String backName = dirName + ".tar";
165+
String command = "tar -cf " + dirName + ".tar " + dirName;
166+
String tarFilePath = parentDir + File.separator + backName;
167+
168+
CommandExecutionUtils.executeCommand(command, fileDir.getParentFile());
169+
String fileZipFilePath = FileCompressUtils.compressFilesByZIP(tarFilePath, parentDir);
170+
LOGGER.info("备份文件夹压缩后fileZipFilePath={},压缩前的打包文件tarFilePath={},tarFilePath删除状态={}", fileZipFilePath, tarFilePath, new File(tarFilePath).delete());
171+
172+
return new File(fileZipFilePath);
173+
}
174+
175+
/**
176+
* 从压缩文件恢复文件夹
177+
*/
178+
@Override
179+
public void dirRestore(MultipartFile[] multipartFiles, String restoreDir) throws Exception{
180+
String restoreFileName = "";
181+
182+
for (MultipartFile multipartFile : multipartFiles) {
183+
InputStream inputStream = multipartFile.getInputStream();
184+
185+
String fileName = StringUtils.isBlank(multipartFile.getOriginalFilename()) ?
186+
System.currentTimeMillis()+"tar.zip" : multipartFile.getOriginalFilename();
187+
File tmpFile = new File(restoreDir + File.separator + fileName);
188+
if(!tmpFile.getParentFile().exists()) {
189+
tmpFile.getParentFile().mkdirs();
190+
}
191+
192+
BufferedInputStream bis = new BufferedInputStream(inputStream);
193+
//接收tar.zip文件流到restoreDir目录
194+
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tmpFile));
195+
//FileOutputStream fileOutputStream = new FileOutputStream(restoreDir + File.separator + fileName);
196+
int len;
197+
byte[] bytes = new byte[1024];
198+
while ((len = bis.read(bytes)) != -1) {
199+
bos.write(bytes, 0, len);
200+
}
201+
bos.close();
202+
bis.close();
203+
204+
//解压
205+
CommandExecutionUtils.executeCommand("unzip " + fileName, tmpFile.getParentFile());
206+
//解包
207+
CommandExecutionUtils.executeCommand("tar -xvf " + fileName.substring(0, fileName.lastIndexOf(".zip"))
208+
+ tmpFile.getParentFile(), tmpFile.getParentFile());
209+
210+
restoreFileName += multipartFile.getOriginalFilename();
211+
}
212+
213+
LOGGER.info("文件夹恢复完成-恢复至restoreDir={},restoreFileName={}", restoreDir, restoreFileName);
214+
}
215+
216+
152217
/**
153218
* 获取磁盘信息
154219
*/

blog-business-vue2/src/api/system/system.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,20 @@ function databaseBackup(databaseName) {
1212
url: '/system/databaseBackup',
1313
method: 'get',
1414
params: { "databaseName":databaseName },
15-
responseType: 'blob' //axios支持文件下载的关键
15+
responseType: 'blob', //axios支持文件下载的关键
16+
timeout: 600000, //10 minutes,因为要涉及文件下载,所以将时间设置得长一点
1617
})
1718
}
1819

20+
function dirBackup(fileDirPath) {
21+
return request({
22+
url: '/system/dirBackup',
23+
method: 'get',
24+
params: { "fileDirPath":fileDirPath },
25+
responseType: 'blob', //axios支持文件下载的关键
26+
timeout: 600000, //10 minutes,因为要涉及文件下载,所以将时间设置得长一点
27+
})
28+
}
1929

20-
export default {systemStatus, databaseBackup}
30+
export default {systemStatus, databaseBackup, dirBackup}
2131

blog-business-vue2/src/views/system/data-backup.vue

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<template>
22
<div style="margin: 10px 10px">
3+
<div style="margin: 1px 10px; background-color:#d3d1d1">
4+
<p>注意:</p>
5+
<p> >> 考虑到服务器的带宽大小,非紧急情况,不建议使用本页面的备份功能进行数据备份。
6+
因为下载会很慢,HTTP链接很容易超时造等情况造成下载失败</p>
7+
<p> >> 可以使用本页面的数据恢复功能,前提是要满足对应的恢复条件(文件格式)</p>
8+
</div>
39
<div>
410
<h3>Database Backup</h3>
511
<el-input
@@ -18,19 +24,53 @@
1824
<div style="text-align: center">
1925
<el-upload
2026
class="upload-demo"
21-
ref="upload"
27+
ref="upload4DatabaseRestore"
2228
name="restoreSql"
2329
:action="databaseRestoreUploadURL"
2430
:on-error="databaseRestoreErrFunc"
2531
:on-success="databaseRestoreSuccessFunc"
2632
:auto-upload="false">
27-
<el-button slot="trigger" type="info">Select SQL File</el-button>
33+
<el-button slot="trigger" type="info">Select Zip SQL File</el-button>
2834
<el-button style="margin-left: 10px;" type="success" @click="databaseRestore">Start Restore</el-button>
2935
</el-upload>
3036
</div>
3137
</div>
3238
<hr/>
3339

40+
<div>
41+
<h3>Dir Backup</h3>
42+
<el-input
43+
placeholder="Input Absolute Dir Path (example:/home/kyle/data)" v-model="dirAbsolutePath" minlength="1" maxlength="50" show-word-limit
44+
length="100px" clearable>
45+
</el-input>
46+
47+
<div style="text-align: center; margin: 8px">
48+
<el-button type="primary" @click="dirBackup" round>Start Backup</el-button>
49+
</div>
50+
</div>
51+
<hr/>
52+
53+
<div>
54+
<h3>Dir Restore</h3>
55+
<div style="text-align: center">
56+
<el-input
57+
placeholder="Input Absolute Restore Dir Path (example:/home/kyle/dataRestore)" v-model="restoreDir" minlength="1" maxlength="50" show-word-limit
58+
length="100px" clearable>
59+
</el-input>
60+
<el-upload
61+
class="upload-demo"
62+
ref="upload4DirRestore"
63+
name="restoreFileZip"
64+
:action="dirRestoreUploadURL + '&restoreDir='+restoreDir"
65+
:on-error="dirRestoreErrFunc"
66+
:on-success="dirRestoreSuccessFunc"
67+
:auto-upload="false">
68+
<el-button slot="trigger" type="info" round>Select Zip File</el-button>
69+
<el-button style="margin-left: 10px;" type="success" @click="dirRestore" round>Start Restore</el-button>
70+
</el-upload>
71+
</div>
72+
</div>
73+
<hr/>
3474
</div>
3575
</template>
3676

@@ -43,7 +83,10 @@ export default {
4383
data() {
4484
return {
4585
databaseName: '',
46-
databaseRestoreUploadURL: process.env.VUE_APP_BACKEND_API + '/system/databaseRestore?token='+getToken()
86+
databaseRestoreUploadURL: process.env.VUE_APP_BACKEND_API + '/system/databaseRestore?token='+getToken(),
87+
dirAbsolutePath: '',
88+
dirRestoreUploadURL: process.env.VUE_APP_BACKEND_API + '/system/dirRestore?token='+getToken(),
89+
restoreDir: '',
4790
}
4891
},
4992
methods: {
@@ -69,7 +112,7 @@ export default {
69112
})
70113
},
71114
databaseRestore() {
72-
this.$refs.upload.submit()
115+
this.$refs.upload4DatabaseRestore.submit()
73116
},
74117
databaseRestoreErrFunc(err) {
75118
console.log("数据库恢复文件上传失败:", err)
@@ -85,7 +128,48 @@ export default {
85128
showClose: true,
86129
message: response.message,
87130
});
88-
}
131+
},
132+
133+
dirBackup() {
134+
if(this.dirAbsolutePath === '' || this.dirAbsolutePath === null) {
135+
this.$message.error('Type Img Absolute Path, Please!');
136+
return
137+
}
138+
//基于axios的文件下载:https://www.jb51.net/article/257569.htm
139+
systemAPi.dirBackup(this.dirAbsolutePath).then(res => {
140+
//console.log("调用后端进行图片资源备份:", res)
141+
142+
let filename = res.headers.filename
143+
let blob = new Blob([res.data]);
144+
let url = window.URL.createObjectURL(blob); // 创建 url 并指向 blob
145+
let a = document.createElement('a');
146+
a.href = url;
147+
a.download = filename;
148+
a.click();
149+
window.URL.revokeObjectURL(url); // 释放该 url
150+
}
151+
).catch(err => {
152+
console.log("调用后端进行图片资源备份出现异常:", err)
153+
})
154+
},
155+
dirRestore() {
156+
this.$refs.upload4DirRestore.submit()
157+
},
158+
dirRestoreErrFunc(err) {
159+
console.log("图片资源恢复文件上传失败:", err)
160+
this.$message({
161+
showClose: true,
162+
message: err.message,
163+
type: 'error'
164+
});
165+
},
166+
dirRestoreSuccessFunc(response) {
167+
console.log("图片资源恢复上传文件成功:", response)
168+
this.$message({
169+
showClose: true,
170+
message: response.message,
171+
});
172+
},
89173
}
90174
}
91175

0 commit comments

Comments
 (0)