springboot结合vue对本地文件及其文件夹进行操作
今天自己做了一个小东西,就是springboot结合vue对本地文件及其文件夹进行操作,可以打开本地文件,打开本地的目录信息,下载本地文件,然后还对不同IP的访问做了一个限制,一个是添加了一个白名单,只有白名单上面的IP地址才能访问该指定的网站,另一个就是设置同一个IP地址在一分钟只能访问该IP地址10次,超过10次之后加入黑名单,只有一天之后才能继续访问。IP限制是两个不同的功能,这一篇文章只写对本地文件以及本地文件夹的操作。
前端页面代码:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./js/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
</head>
<body>
<div id="root">
<ul id="fileNames" v-if="false">
<li th:each="fileName : ${fileNames}" th:text="${fileName}"></li>
</ul>
<el-tree default-expand-all @node-contextmenu="handleContextMenu" :data="fileNames" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
<!-- 右击菜单信息-->
<el-dialog
:title="fileInfo.absolutePath"
:visible.sync="dialogVisible"
width="30%">
<el-card class="box-card">
<div style="margin-top: 5px; cursor: pointer" class="text item" v-if="!fileInfo.directory" @click="openFile()">打开文件</div>
<div style="margin-top: 5px; cursor: pointer" class="text item" v-if="!fileInfo.directory" @click="downloadFile()">下载文件</div>
<div style="margin-top: 5px; cursor: pointer" class="text item" v-if="!fileInfo.directory" @click="downloadFile2()">下载文件(方式二)</div>
<el-popover
v-if="fileInfo.directory"
placement="right"
width="400"
trigger="click">
<el-input v-model="newFolderName" placeholder="请输入文件夹名称" size="mini" @blur="cancelCreateFolder" @keyup.enter.native="newFolder" ></el-input>
<div style="margin-top: 5px; cursor: pointer" class="text item" slot="reference">新建文件夹</div>
</el-popover>
</el-card>
</el-dialog>
</div>
</body>
</html>
<script>
// 通过DOM获取后端传递的数据
var messageFromBackend = []
messageFromBackend = document.getElementById("fileNames").innerText;
const vm = new Vue({
el:'#root', // el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
data:{ // data 中用于存储数据,数据供el指定的容器去使用,这些值我们暂时先写成一个对象
fileNames: [],
defaultProps: {
children: 'children',
label: 'name'
},
dialogVisible: false,
dialogVisible2: false,
fileInfo: [],
newFolderName: '',
clickNum: 0,
fileContent: ''
},
methods: {
handleNodeClick(data) {
let _self = this
if (!data.directory) {
// 发起带参数的 GET 请求
axios.post('http://localhost:8888/openFile/', data).then(response => {
}).catch(error => {
console.error('Error fetching data:', error);
});
this.doubleClick = false;
return false;
}
const size = _self.fileNames.indexOf(data)
data.children = null
// 发起带参数的 GET 请求
axios.post('http://localhost:8888/getFiles/', data).then(response => {
if (response.data.length === 0) {
this.$message.error("该目录下已没有其他目录或文件!")
return false;
}
let tempPath = response.data[0].absolutePath
const parts = tempPath.split("\\");
const result = parts.slice(0, -1).join("\\");
this.setAgeRecursively(_self.fileNames, result, response.data);
}).catch(error => {
console.error('Error fetching data:', error);
});
},
setAgeRecursively(data, targetName, newAge) {
for (let i = 0; i < data.length; i++) {
if (data[i].absolutePath === targetName) {
data[i].children = newAge;
return true; // 找到目标并赋值后退出递归
}
if (data[i].children && data[i].children.length > 0) {
if (this.setAgeRecursively(data[i].children, targetName, newAge)) {
return true; // 找到目标并赋值后退出递归
}
}
}
return false; // 未找到目标对象
},
handleContextMenu(node, event) {
this.fileInfo = event;
this.dialogVisible = true;
},
// 打开文件
openFile() {
let dataInfo = this.fileInfo
// 发起带参数的 POST 请求
axios.post('http://localhost:8888/openFile/', dataInfo).then(response => {
}).catch(error => {
console.error('Error fetching data:', error);
});
this.fileInfo = []
this.dialogVisible = false;
},
// 下载文件
downloadFile() {
let dataInfo = this.fileInfo
// 发起带参数的 POST 请求
axios.post('http://localhost:8888/downloadFile/', dataInfo).then(response => {
}).catch(error => {
console.error('Error fetching data:', error);
});
this.fileInfo = []
this.dialogVisible = false;
},
downloadFile2() {
let dataInfo = this.fileInfo
// 发起带参数的 POST 请求
axios.post('http://localhost:8888/downloadFile2/', dataInfo).then(response => {
responseFileData = response.config.data;
responseFileData = JSON.parse(responseFileData)
// 创建一个 Blob 对象
const blob = new Blob([response.data], {
type: response.headers["content-type"]
});
// 创建一个下载链接并点击
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = responseFileData.name; // 替换为实际的文件名和扩展名
link.click();
// 释放资源
URL.revokeObjectURL(url);
}).catch(error => {
console.error('Error fetching data:', error);
});
this.fileInfo = []
this.dialogVisible = false;
},
// 新建文件夹
newFolder() {
let dataInfo = this.fileInfo
dataInfo.newFolderName = this.newFolderName; console.log(dataInfo)
dataInfo.children = null
// 发起带参数的 POST 请求
axios.post('http://localhost:8888/createFolder/', dataInfo).then(response => {
}).catch(error => {
console.error('Error fetching data:', error);
});
this.fileInfo = []
this.newFolderName = '';
this.dialogVisible = false;
},
// 新建文件夹时input标签失去焦点后触发的方法
cancelCreateFolder() {
this.newFolderName = '';
this.dialogVisible = false;
this.fileInfo = []
}
},
created() {
messageFromBackend = JSON.parse(messageFromBackend)
this.fileNames = messageFromBackend;
}
})
</script>
前提:准备工作,创建好实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileInfo {
private String name; // 文件或者文件夹的名称
private String absolutePath; // 文件或者文件夹的绝对路径
private FileInfo children; // 文件夹下面的子文件夹
private String newFolderName; // 新建文件夹时的名称
private boolean isDirectory; // 判断是否为文件夹
public void setIsDirectory(boolean directory) {
isDirectory = directory;
}
}
功能一:后端把指定目录下面的数据发送给前端,然后前端接收到数据,并展示到页面上
后端代码
@Controller
public class FileShowController {
@Autowired
private ObjectMapper objectMapper;
@GetMapping("/info")
public String showFiles(Model model) throws JsonProcessingException {
// 指定要展示的文件夹路径
String path = "C:/";
List<FileInfo> fileList = new ArrayList<>();
File directory = new File(path);
// 这里是通过 for 循环读取这个路径下的所有文件
if (directory.isDirectory()) { // 判断这个路径是否为文件夹
File[] files = directory.listFiles(); // 获取到这个路径下的所有文件信息
if (files != null) {
for (File file : files) {
FileInfo fileInfo = new FileInfo();
fileInfo.setName(file.getName()); // 获取这个文件的名称
fileInfo.setIsDirectory(file.isDirectory()); // 判断这个文件是否为一个文件夹
fileInfo.setAbsolutePath(file.getAbsolutePath()); // 获取这个文件的绝对路径
fileList.add(fileInfo); // 把这个文件的信息保存在定义好的 List 数组里面
}
}
}
// 这里是将 Java 对象转换为一个 json 格式的字符串
String info = objectMapper.writeValueAsString(fileList);
// 将文件名列表传递给模板,用于前端接收
model.addAttribute("fileNames", info);
// 返回的是前端页面的名称,作用就是去到 fileList.html 这个页面
return "fileList";
}
}
注意:因为这里是需要去到页面,所以这个类的注解是 @Controller,而不是 @RestController
前端代码
页面代码
<html xmlns:th="http://www.thymeleaf.org">
<!-- 注意,前端想接收到数据必须引入这个模块 -->
<div id="root">
<!-- 这里是接收后端发送过来数据的,然后通过 js 把数据存放到 vue 里面的 data 中 -->
<!-- 所以这个不需要显示,所有需要显示的信息,由 element-ui 的组件来展示 -->
<ul id="fileNames" v-if="false">
<li th:each="fileName : ${fileNames}" th:text="${fileName}"></li>
</ul>
<el-tree default-expand-all :data="fileNames" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</div>
js 代码
<script>
// 通过DOM获取后端传递的数据
// 这里就是通过标签获取到标签中的数据,然后把数据保存在 messageFromBackend 数组里面
var messageFromBackend = []
messageFromBackend = document.getElementById("fileNames").innerText;
const vm = new Vue({
el:'#root',
data:{
fileNames: [],
defaultProps: {
children: 'children',
label: 'name'
}
},
methods: {
handleNodeClick(data) {
}
},
created() {
// 这里就是把数据存放到 vue 中,然后 vue 就可以使用后端传递过来的数据了
messageFromBackend = JSON.parse(messageFromBackend)
this.fileNames = messageFromBackend;
}
})
</script>
问题1:大家可以思考一下为什么要这样接收数据
功能二:点击一个目录,则会展示该目录下的内容
@PostMapping("/getFiles")
@ResponseBody
public List<FileInfo> getFiles(@RequestBody FileInfo info) throws JsonProcessingException {
String path = info.getAbsolutePath();
ArrayList<FileInfo> fileList = new ArrayList<>();
File directory = new File(path);
if (directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
FileInfo fileInfo = new FileInfo();
fileInfo.setName(file.getName());
fileInfo.setIsDirectory(file.isDirectory());
fileInfo.setAbsolutePath(file.getAbsolutePath());
fileList.add(fileInfo);
}
}
}
return fileList;
}
功能三:打开一个点击的文件
@PostMapping("/openFile")
public String openFile(@RequestBody FileInfo fileInfo) {
// 先创建一个 byte 数组,然后把读取到的文件内容保存在这个数组里面
// 这个数组作为一个临时文件
byte[] fileData = new byte[0];
String filePath = fileInfo.getAbsolutePath();
System.err.println(filePath);
try {
File file = new File(filePath);
fileData = new byte[(int) file.length()];
try (FileInputStream inputStream = new FileInputStream(file)) {
inputStream.read(fileData);
}
// 现在 fileData 包含了文件的字节数组数据
// 可以在这里处理 fileData
try {
File tempFile = File.createTempFile("temp", ".tmp");
try (FileOutputStream outputStream = new FileOutputStream(tempFile)) {
outputStream.write(fileData);
}
filePath = tempFile.getAbsolutePath();
// 根据操作系统不同,设置合适的命令来打开文件
String osName = System.getProperty("os.name").toLowerCase();
String command;
if (osName.contains("win")) {
command = "cmd /c start " + filePath;
} else if (osName.contains("mac") || osName.contains("nix") || osName.contains("nux") || osName.contains("bsd")) {
command = "open " + filePath;
} else {
throw new UnsupportedOperationException("Unsupported operating system");
}
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
return "fileList";
}
功能四:在一个目录下面创建一个文件夹
@PostMapping("/createFolder")
@ResponseBody
public String createFolder(@RequestBody FileInfo fileInfo) {
// 得到的是需要创建文加的名称
String folderName = fileInfo.getNewFolderName();
// 得到的文件夹的路劲
String parentDirectoryPath = fileInfo.getAbsolutePath(); // 替换为你要创建文件夹的父目录路径
Path newFolderPath = Paths.get(parentDirectoryPath, folderName);
try {
// 创建文件夹
Files.createDirectories(newFolderPath);
return "Folder created: " + newFolderPath;
} catch (IOException e) {
e.printStackTrace();
return "Failed to create folder.";
}
}
功能五:下载文件
方法一:下载到指定的路径
@PostMapping("/downloadFile")
public void downloadFile(@RequestBody FileInfo fileInfo, HttpServletResponse response) throws IOException {
String sourceFilePath = fileInfo.getAbsolutePath(); // 替换为你要下载的源文件路径
String destinationDirectoryPath = "C:\\user\\新建文件夹"; // 替换为你要下载到的目标目录路径
File sourceFile = new File(sourceFilePath);
File destinationDirectory = new File(destinationDirectoryPath);
if (sourceFile.exists() && destinationDirectory.exists() && destinationDirectory.isDirectory()) {
Path destinationFilePath = Paths.get(destinationDirectoryPath, sourceFile.getName());
try {
Files.copy(sourceFile.toPath(), destinationFilePath);
response.getWriter().write("File downloaded to: " + destinationFilePath);
} catch (Exception e) {
e.printStackTrace();
}
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
System.out.println("下载成功");
}
方法二:通过浏览器下载到自己选中的路径
@PostMapping("/downloadFile2")
public ResponseEntity<Resource> downloadFile2(@RequestBody FileInfo fileInfo, HttpServletResponse response) throws IOException {
String filename = fileInfo.getName();
String filePath = fileInfo.getAbsolutePath();
Path file = Paths.get(filePath);
Resource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
return ResponseEntity.ok()
.headers(headers)
.contentLength(resource.contentLength())
.body(resource);
}
总结: 这样一个本地文件夹相关的操作已经简单的完成,然后因为我只是实现了一个简单的功能,一些细节方面没有处理,所以如果大家要用到实际当中,可以在此基础上再次进行优化,比如,把一些路径写到 application.yml 里面,然后再通过 @Value 注解引用,这样以后需要改动一些路径也会方便许多,然后数据返回的格式也可以统一一下,然后前端页面成功与失败可以提供一个提示信息
这里我对上面传输数据的方式表达一下我的见解,因为我也只是一个编程菜鸟,考虑不足望搭建见谅,如果大家有不同的想法,也欢迎大家在评论区留言!!!
因为我浏览器访问的是后端的地址,然后再由后端的返回值返回到前端页面,所以这里就不能返回 json 数据,只能返回前端页面名称,然后数据传输方式我就选择了 model.addAttribute 的方式,然后前端我就选择了使用 thymeleaf 模块来接收数据。然后再通过原生 js 获取到节点上的数据,然后把数据传输到 vue
提供的改善方法:
大家可以在浏览器直接访问页面,然后在 vue 的创建生命周期函数中对后端发起请求,这样后端就可以发送 json 数据,然后再由后端的 vue 接收,这样就换成了大家常用的数据发送以及接收的方式
如果大家有其他见解,欢迎大家给我留言!!!