远程文件管理系统(SpringBoot + Vue)

时间:2024-01-28 19:00:50

一、简介


可以实现对本地文件的 增、删、改、重命名等操作的监控,通过登录远程文件监控系统,获取一段时间内本地文件的变化情况。

系统功能图如下:
image.png
流程图如下:
网安流程图.png

二、本地文件监控程序的实现(C++)


调用 windows api 监控本地文件操作,将对应的文件操作上传到远程数据库端。

#include <Windows.h>
#include <string>
#include <iostream>
#include <iomanip>
#include <tchar.h>
#include <winsock.h>
#include "include/mysql.h"
#include <ctime>
#include <thread>

#pragma comment(lib,"lib/libmysql.lib")
#pragma comment(lib,"lib/mysqlclient.lib")


using namespace std;



/*
通过CreateFile函数打开监控目录,获取监控目录的句柄
API函数ReadDirecotryChangesW,实现文件监控操作
*/

//字符串替换(全部)
string replace(string& base, string src, string dst)
{
	int pos = 0, srclen = src.size(), dstlen = dst.size();
	while ((pos = base.find(src, pos)) != string::npos)
	{
		base.replace(pos, srclen, dst);
		pos += dstlen;
	}
	return base;
}



//void DirectoryMonitoring();
void DirectoryMonitoring(const TCHAR * disk,MYSQL &mysql)
{
	//cout << __FUNCTION__ << " is called." << endl;   //__FUNCTION__,当前被调用的函数名
	string sql;
	///mysql下面



	DWORD cbBytes;  //Double Word  Windows.h中
	char file_Name[MAX_PATH]; //设置文件名
	char file_Name2[MAX_PATH]; //设置文件重命名后的名字
	char notify[1024];
	int count = 0; //文件操作次数
	TCHAR *dir =(TCHAR *) _T(disk);    //_T 确保编码的兼容性,磁盘名

	//调用CreateFile(Win Api)来获得指向一个物理硬盘的句柄,CreateFile函数打开监控目录,获取监控目录的句柄。
	HANDLE dirHandle = CreateFile(dir, GENERIC_READ | GENERIC_WRITE | FILE_LIST_DIRECTORY,  //访问模式对设备可以读写数据
		FILE_SHARE_READ | FILE_SHARE_WRITE,    //共享模式可读可写
		NULL,       //文件的安全特性,无
		OPEN_EXISTING,    //文件必须已经存在,若不存在函数返回失败
		FILE_FLAG_BACKUP_SEMANTICS,   //文件属性
		NULL);  //用于复制文件句柄

	if (dirHandle == INVALID_HANDLE_VALUE) //是否成功
	{
		cout << "error" + GetLastError() << endl;
	}


	memset(notify, 0, strlen(notify));  //给notify赋值为0,即清空数组

	FILE_NOTIFY_INFORMATION *pnotify = (FILE_NOTIFY_INFORMATION*)notify;   //结构体FILE_NOTIFY_INFORMATION,存储文件操作信息其action属性
	cout << "正在监视文件" << endl;
	while (true)
	{

		if (ReadDirectoryChangesW(dirHandle, &notify, 1024, true,  //对目录进行监视的句柄;一个指向FILE_NOTIFY_INFORMATION结构体的缓冲区,其中可以将获取的数据结果将其返回;lpBuffer的缓冲区的大小值,以字节为单位;是否监视子目录. 
			FILE_NOTIFY_CHANGE_FILE_NAME |
			FILE_NOTIFY_CHANGE_DIR_NAME
			| FILE_NOTIFY_CHANGE_SIZE,                //对文件过滤的方式和标准
			&cbBytes, NULL, NULL))                  //将接收的字节数转入lpBuffer参数
		{
			//宽字节转换为多字节
			if (pnotify->FileName)
			{
				memset(file_Name, 0, strlen(file_Name));

				WideCharToMultiByte(CP_ACP, 0, pnotify->FileName, pnotify->FileNameLength / 2, file_Name, 99, NULL, NULL);
			}

			//重命名的文件名
			if (pnotify->NextEntryOffset != 0 && (pnotify->FileNameLength > 0 && pnotify->FileNameLength < MAX_PATH))
			{
				PFILE_NOTIFY_INFORMATION p = (PFILE_NOTIFY_INFORMATION)((char*)pnotify + pnotify->NextEntryOffset);
				memset(file_Name2, 0, sizeof(file_Name2));
				WideCharToMultiByte(CP_ACP, 0, p->FileName, p->FileNameLength / 2, file_Name2, 99, NULL, NULL);
			}

			string str=file_Name;
			str = replace(str, "\\", "/");
			str = dir+str;
			
			string str2=file_Name2;
			str2 = replace(str2, "\\", "/");
			str2 = dir+str2;
			string link = "-->";

			//设置类型过滤器,监听文件创建、更改、删除、重命名等
			switch (pnotify->Action)
			{
			case FILE_ACTION_ADDED:  //添加文件
				count++;
				cout << count << setw(5) << "File Add:" << setw(5) << file_Name << endl;
				sql = "begin;";
				mysql_query(&mysql, sql.c_str());
				sql = "insert into file_info(action,name)\
                values (\"File Added\",\""+str+"\");";
				if (mysql_query(&mysql, sql.c_str()))
				{
					cout << "line: " << __LINE__ << ";" << mysql_error(&mysql) << mysql_errno(&mysql) << endl;
				}
				sql = "commit;";
				mysql_query(&mysql, sql.c_str());
				break;
			case FILE_ACTION_MODIFIED:   //修改文件
				cout << "File Modified:" << setw(5) << file_Name << endl;
				sql = "begin;";
				mysql_query(&mysql, sql.c_str());
				sql = "insert into file_info(action,name)\
                values (\"File Modified\",\"" + str + "\");";
				if (mysql_query(&mysql, sql.c_str()))
				{
					cout << "line: " << __LINE__ << ";" << mysql_error(&mysql) << mysql_errno(&mysql) << endl;
				}
				sql = "commit;";
				mysql_query(&mysql, sql.c_str());
				break;
			case FILE_ACTION_REMOVED:  //删除文件
				count++;
				cout << count << setw(5) << "File Removed:" << setw(5) << file_Name << endl;
				sql = "begin;";
				mysql_query(&mysql, sql.c_str());
				sql = "insert into file_info(action,name)\
                values (\"File Deleted\",\"" + str + "\");";
				if (mysql_query(&mysql, sql.c_str()))
				{
					cout << "line: " << __LINE__ << ";" << mysql_error(&mysql) << mysql_errno(&mysql) << endl;
				}
				sql = "commit;";
				mysql_query(&mysql, sql.c_str());
				break;
			case FILE_ACTION_RENAMED_OLD_NAME:   //重命名
				cout << "File Renamed:" << setw(5) << file_Name << "->" << file_Name2 << endl;
				sql = "begin;";
				mysql_query(&mysql, sql.c_str());
				sql = "insert into file_info(action,name)\
                values (\"File Renamed\",\"" + str+link+str2 + "\");";
				if (mysql_query(&mysql, sql.c_str()))
				{
					cout << "line: " << __LINE__ << ";" << mysql_error(&mysql) << mysql_errno(&mysql) << endl;
				}
				sql = "commit;";
				mysql_query(&mysql, sql.c_str());
				break;

			default:
				cout << "未知命令" << endl;

			}

		}

	}
	CloseHandle(dirHandle);
}

int _tmain(int argc, _TCHAR* argv[])
{
	const TCHAR * disk1 = _T("C://");
	const TCHAR * disk2 = _T("D://");
	const TCHAR * disk3 = _T("E://");
	MYSQL mysql;

	mysql_init(&mysql);
	// 连接远程数据库
	if (NULL == mysql_real_connect(&mysql, "database_host", "username", "password", "mysql", 3306, NULL, 0))
	{
		cout << __LINE__ << mysql_error(&mysql) << mysql_errno(&mysql) << endl;
		throw - 1;
	}


	//进入数据库hr_1
	string sql = "use hr_1;";
	if (mysql_query(&mysql, sql.c_str()))
	{
		cout << "line: " << __LINE__ << ";" << mysql_error(&mysql) << mysql_errno(&mysql) << endl;
		throw - 1;
	}
	mysql_query(&mysql, "SET NAMES GBK");  //数据库编码格式

	thread t1(DirectoryMonitoring, disk1,ref(mysql));
	thread t2(DirectoryMonitoring, disk2,ref(mysql));
	thread t3(DirectoryMonitoring, disk3,ref(mysql));
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

三、后端的实现(Java)


后端框架:SpringBoot    依赖:mybatis、lombok

文件信息类:

package com.example.file_monitor;
import lombok.Data;
import lombok.NoArgsConstructor;

/*
文件信息实体对象
 */
@Data
@NoArgsConstructor
public class FileInfo {
    private int id;
    private String action;
    private String name;
    private String time;

    public FileInfo(int id,String action,String name,String time){
        this.id=id;
        this.action=action;
        this.name=name;
        this.time=time;
    }
}

数据库操作接口:

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/*
数据库操作接口
 */
@Mapper
public interface FileMapper {
    //获取文件监控信息列表
    @Select("SELECT * FROM file_info")
    List<FileInfo> findAllFile();

    //分页获取
    @Select("SELECT * FROM file_info LIMIT #{start},#{end}")
    List<FileInfo> findFile(@Param("start") int start,@Param("end") int end);

    //删除
    @Delete("DELETE FROM file_info WHERE id= #{id}")
    int deleteFile(@Param("id") int id);

    //统计各种操作
    @Select("SELECT COUNT(*) FROM file_info WHERE action= #{a}")
    int getCount(@Param("a") String action);
}

控制类

package com.example.file_monitor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping(value = "api")
public class FileController {

    @Autowired
    private FileMapper fileMapper;

    @Autowired
    private ApiJson apiJson;

    //分页
    @GetMapping("/data")
    public ApiJson getFileList(@RequestParam("curr") int page,@RequestParam("nums") int limit){
        int start=(page-1)*limit;
        int end= limit;
        List<FileInfo> data=fileMapper.findFile(start,end);
        apiJson.setCode(0);
        apiJson.setCount(100);
        apiJson.setMsg("test");
        apiJson.setData(data);
        return apiJson;
    }

    //统计
    @GetMapping("/count")
    public List<Integer> getCount(){
        int mod=fileMapper.getCount("File Modified");
        int add=fileMapper.getCount("File Added");
        int dele=fileMapper.getCount("File Deleted");
        int reName=fileMapper.getCount("File Renamed");
        List<Integer> res=new ArrayList<Integer>();
        res.add(mod);
        res.add(add);
        res.add(dele);
        res.add(reName);
        return res;
    }

    //删除
    @CrossOrigin
    @GetMapping("/delete")
    public int delFile(@RequestParam("id") int id){
        return fileMapper.deleteFile(id);
    }

    //获取所有信息
    @CrossOrigin
    @GetMapping("/all")
    public  List<FileInfo> getAllFileInfo(){
        return fileMapper.findAllFile();
    }
}

若前端使用 layui 框架,需要 json 格式的数据,所以利用该类生成 json 数据

package com.example.file_monitor;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@NoArgsConstructor
@Component
public class ApiJson {
    private int code;
    private String msg;
    private int count;
    private List<FileInfo> data;

    public ApiJson(int code,String msg,int count,List<FileInfo> data){
        this.code=code;
        this.msg=msg;
        this.count=count;
        this.data=data;
    }

}

四、前端实现(layui)


借助 ajax 与后端进行数据交换
例如:

    function sendAjaxGet() {
        $.ajax({
            type: "GET",
            url: "/api/count",
            success: function(data){
                Chart(data[0],data[1],data[2],data[3]);
            },
            error: function (message) {

            }
        });
    }
    sendAjaxGet();

借助 layui table 实现表格的生成 layui 表格

借助 Echarts 实现统计图的生成    echarts

详情见 github 项目:(还没上传)

image.png

五、前端实现(Vue)


5.1 简介

之前使用的是 layui 搭建前端,最近在学 Vue,所以打算利用 Vue 前后端分离重写一下前端。

目录结构:
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1239731/1614932934224-303a7cab-e33d-40f2-a050-09f95c293bdf.png#align=left&display=inline&height=254 &originHeight=344&originWidth=361&size=17161&status=done&style=none&width=267)远程文件监控系统初稿.png

5.2 FileMon.vue

FileMon 中划分为三大部分:头部(导航栏NavMenu)、侧边栏(图Echart)、main (表格)。

<template>
  <el-container>
    <el-header>
      <NavMenu></NavMenu>
    </el-header>

    <el-container>
      <el-aside width="500px">
        <Echart ref="ac_e"></Echart>
      </el-aside>
      <el-container>
        <el-main>
          <Table ref="ac_t" @exchange="exchange()"></Table>
        </el-main>
      </el-container>
    </el-container>

  </el-container>
</template>

<script>
  import NavMenu from \'./common/NavMenu.vue\'
  import Table from \'./common/Table.vue\'
  import Echart from \'./common/Echart.vue\'

  export default {
    name: \'FileMon\',
    components:{ NavMenu,Table,Echart },
    methods: {
      exchange: function() {
        this.$refs.ac_e.$data.ac = this.$refs.ac_t.$data.ac
        this.$refs.ac_e.loads()
      }
    }
  }
</script>

<style>
</style>

5.3 NavMenu

<template>

  <el-menu
    :default-active="activeIndex2"
    class="el-menu-demo"
    mode="horizontal"
    background-color="#545c64"
    text-color="#fff"
    active-text-color="#ffd04b">

    <el-menu-item index="1"></el-menu-item>
    <div style="right: 650px;position: fixed; color: #EEEEEE;top: 4px;">
      <h2>远程文件监控系统</h2>
    </div>
  </el-menu>

</template>

<script>
  export default {
    name: \'NavMenu\'
  }
</script>

<style>
</style>

5.4 表格

<template>
    <el-table
      height="500"
      :data="tableData.filter(data => !search || data.name.toLowerCase().includes(search.toLowerCase()))"
      style="width: 100%">
      <el-table-column
        label="ID"
        prop="id">
      </el-table-column>
      <el-table-column
        label="操作类型"
        prop="action">
      </el-table-column>
      <el-table-column
        label="文件名"
        prop="name">
      </el-table-column>
      <el-table-column
        label="日期"
        prop="time">
      </el-table-column>
      <el-table-column
        align="right">
        <template slot="header" slot-scope="scope">
          <el-input
            v-model="search"
            size="mini"
            placeholder="输入关键字搜索"/>
        </template>
        <template slot-scope="scope">
          <el-button
            size="mini"
            @click="handleEdit(scope.$index, scope.row)">Edit</el-button>
          <el-button
            size="mini"
            type="danger"
            @click="handleDelete(scope.$index, scope.row)">Delete</el-button>
        </template>
      </el-table-column>
    </el-table>
</template>

<script>
  export default {
    name: \'Table\',
    data: function() {
      return {
        tableData: [],
        search: \'\',
        ac: [0,0,0,0]   //统计文件各类操作数量,供绘制饼图
      }
    },
    methods: {
      handleEdit(index, row) {
        console.log(index, row)
      },
      handleDelete(index, row) {
        this.$axios.get(\'delete?id=\'+row.id).then(resp =>{
          console.log(resp.data)
          if( resp.data != 0){
            this.$alert(\'删除成功\')
            this.loads()    //每次删除后更新一下表格中的数据
          }
        })
      },
      loads(){
        this.$axios.get(\'/all\').then(resp =>{
          if( resp){
            this.tableData = resp.data
            this.counts()     //每次更新表格数据后,统计各类操作数量
            this.$emit(\'exchange\')
          }
        })
      },
      counts(){
        var i
        for( i in this.tableData){   //这个 for 循环 i 是列表tableData的索引
          if(this.tableData[i].action == \'File Added\') {this.ac[0] = this.ac[0]+1}
          else if(this.tableData[i].action == \'File Deleted\') {this.ac[1] = this.ac[1]+1}
          else if(this.tableData[i].action == \'File Modified\') {this.ac[2] = this.ac[2]+1}
          else  {this.ac[3] = this.ac[3]+1}
        }
      }
    },
    mounted:function(){
      this.loads()
    }
  }
</script>

<style>
</style>

5.5 饼图

<template>
      <div>
      <!--卡片视图区域-->
      <el-card>
          <!-- 2、为ECharts准备一个具备大小(宽高)的Dom -->
          <div id="main" style="width: 600px;height:500px;"></div>
      </el-card>
      </div>
</template>
<script>
import * as echarts from \'echarts\'   //引入 echarts

export default {
  name: \'Echart\',
  data: function () {
      return {
        ac: [0,0,0,0]
      }
    },
  methods:{
    loads: function() {
      // 3、基于准备好的dom,初始化echarts实例
      var myChart = echarts.init(document.getElementById(\'main\'))
      // 4、准备数据和配置项
      // 指定图表的配置项和数据
      var option = {
              title: {
                  text: \'文件监控信息\',
                  left: \'center\'
              },
              tooltip: {
                  trigger: \'item\'
              },
              legend: {
                  orient: \'vertical\',
                  left: \'left\',
              },
              series: [
                  {
                      name: \'访问来源\',
                      type: \'pie\',
                      radius: \'50%\',
                      data: [
                          {value: this.ac[0], name: \'添加文件\'},
                          {value: this.ac[1], name: \'删除文件\'},
                          {value: this.ac[2], name: \'修改文件\'},
                          {value: this.ac[3], name: \'重命名\'},
                      ],
                      emphasis: {
                          itemStyle: {
                              shadowBlur: 10,
                              shadowOffsetX: 0,
                              shadowColor: \'rgba(0, 0, 0, 0.5)\'
                          }
                      }
                  }
              ]
      }
      // 5、展示数据
      // 使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
    }
  },
  mounted: function() {
    this.loads()
  }
}
</script>

5.6 FileMon--表格--饼图 之间的关系

Table.vue 中的 loads 函数,每次执行时 counts() 函数更新 ac 变量的值,并定义触发 exchange 事件。

      loads(){
        this.$axios.get(\'/all\').then(resp =>{
          if( resp){
            this.tableData = resp.data
            this.counts()
            this.$emit(\'exchange\')
          }
        })
      },

FileMon.vue 监听 exchange 事件,触发时执行 exchange 函数

    <el-container>
      <el-aside width="500px">
        <Echart ref="ac_e"></Echart>
      </el-aside>
      <el-container>
        <el-main>
          <Table ref="ac_t" @exchange="exchange()"></Table>
        </el-main>
      </el-container>
    </el-container>

exchange 函数 取 Echart.vue 中的 ac 变量 赋值为 Table.vue 中的 ac 变量,调用 Echart.vue 变量的 load 方法。

    methods: {
      exchange: function() {
        this.$refs.ac_e.$data.ac = this.$refs.ac_t.$data.ac
        this.$refs.ac_e.loads()
      }
    }
  }

5.7 main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from \'vue\'
import App from \'./App\'
import router from \'./router\'
import axios from \'axios\'   //引入axios

//设置代理
axios.defaults.baseURL = \'http://localhost:8443/api\'
//注册全局
Vue.prototype.$axios = axios

import ElementUI from \'element-ui\';
import \'element-ui/lib/theme-chalk/index.css\';

Vue.config.productionTip = false
Vue.use(ElementUI);



/* eslint-disable no-new */
new Vue({
  el: \'#app\',
  router,
  components: { App },
  template: \'<App/>\'
})

5.8 路由

import Vue from \'vue\'
import Router from \'vue-router\'
import HelloWorld from \'@/components/HelloWorld\'
import FileMon from \'@/components/FileMon\'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: \'/\',
      name: \'HelloWorld\',
      component: HelloWorld
    },
    {
      path: \'/fm\',
      name: \'FileMon\',
      component: FileMon
    }
  ]
})