遠端檔案管理系統(SpringBoot + Vue)

WTa0發表於2021-03-14

一、簡介


可以實現對本地檔案的 增、刪、改、重新命名等操作的監控,通過登入遠端檔案監控系統,獲取一段時間內本地檔案的變化情況。

系統功能圖如下:
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
    }
  ]
})

相關文章