基于vue与springboot的sftp远端服务器文件管理系统

1.废话不多说直接看演示视频(视频没有声音各位谅解)

该项目可以实现对远端服务器文件的(linux系统),上传,下载,浏览。

数据自助平台演示

2.项目代码

 前端

main.js 

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./router/index.js";
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

// createApp(App).use(router).use(ElementPlus).mount('#app')
const app = createApp(App);

//app.config.globalProperties.$ip = 'http://192.168.36.14:8777';
//app.config.globalProperties.$ip = 'http://192.168.188.129:8777';
app.config.globalProperties.$ip = 'http://localhost:8777';
app.config.globalProperties.$username = 'wxa'
app.config.globalProperties.$password = 'e10adc3949ba59abbe56e057f20f883e' //123456
app.config.globalProperties.$path = '/data'

app.use(router).use(ElementPlus).mount('#app');

login.vue

<template class="parent">
  <div class="child">
    <div style="width: 400px; margin-top: 100px">
      <h1 style="text-align: center; margin-bottom: 30px">数据自助</h1>
      <el-form :model="user" :rules="rules" size="large" ref="ruleFormRef">
        <el-form-item prop="name">
          <el-input v-model="user.name" :prefix-icon="User"/>
        </el-form-item>
        <el-form-item prop="password">
          <el-input v-model="user.password" :prefix-icon="Lock" show-password/>
        </el-form-item>
        <el-form-item>
          <el-select v-model="path" placeholder="选择打开路径">
            <el-option
                v-for="item in options"
                :key="item.value"
                :label="item.label"
                :value="item.value"
                :disabled="item.disabled"
            />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" style="width: 100%" @click="login">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script setup>
import {getCurrentInstance, reactive, ref} from "vue";
import {ElNotification} from "element-plus";
import router from "../router/index.js";
import {User, Lock} from '@element-plus/icons-vue'
import md5 from 'js-md5'
const ss = getCurrentInstance()?.appContext.config.globalProperties.$path
const path = ref(ss)
//const path = ref("/data")

const options = [
  {
    value: "/opt",
    label: "/opt",
  },
  {
    value: "/data",
    label: "/data",
  },
  {
    value: "/",
    label: "/",
    // disabled: true,
  },
  {
    value: "/home",
    label: "/home",
  },
]

const rules = reactive({
  username:[
    {required:true, message:'请输入用户名', trigger:'blur'}
  ],
  password:[
    {required:true, message:'请输入密码', trigger:'blur'}
  ]
})
const user =reactive({

})
const admin = reactive({
  name: getCurrentInstance()?.appContext.config.globalProperties.$username,
  password: getCurrentInstance()?.appContext.config.globalProperties.$password
})

const login = () => {
  //const pass = CryptoJS.MD5(user.password).toString();
  const pass = md5(user.password);
  if(user.name !== admin.name){
    ElNotification.error("用户名错误")
  }else if(pass !== admin.password){
    ElNotification.error("密码错误")
  }else {
    ElNotification.success("登录成功")
    sessionStorage.setItem('username', JSON.stringify(admin.name))
    sessionStorage.setItem('path', JSON.stringify(path.value))
    // localStorage.setItem('username', JSON.stringify(admin.name))
    router.push("/SftpHome")
  }
}
</script>

<style scoped>

.child {
  position:absolute;
  left:50%;
  transform:translateX(-50%);
}
.parent {
  position:relative;
}

</style>

sftpHome.vue

<template class="parent">
  <div class="child" style="height: 100%">
    <el-container style="margin-right: 700px">
      <el-upload :action= ip :limit="3" :data={selectIndex:state.nextPath} :on-success="handleUploadSuccess">
        <el-button type="primary" style="font-size: 20px"><el-icon><CirclePlus /></el-icon>上传文件 <i class="el-icon-circle-plus-outline"></i></el-button>
      </el-upload>
      <el-button type="warning" @click="addfold" style="font-size: 20px"><el-icon><FolderAdd /></el-icon>新建文件夹 <i class="el-icon-circle-plus-outline"></i></el-button>
      <el-button type="success" @click="upStep" style="font-size: 20px; margin-left: 0px"><el-icon><House /></el-icon>返回首页</el-button>
    </el-container>
    <el-table :data="filterTableData" style="font-size: 20px;width:100%;height: 100%" :row-style="{height: '20px'}" :cell-style="{padding:'0px'}"
              scrollbar-always-on="true"
              flexible="true"
              size="large"
    >

      <el-table-column label="文件类型" prop="type" >
        <template v-slot="scope">
          <h2 v-if="scope.row.type === 1"><el-icon><DocumentCopy /></el-icon></h2>
          <h2 v-if="scope.row.type === 2"><el-icon><Folder /></el-icon></h2>
        </template>
      </el-table-column>
      <el-table-column label="创建时间" prop="creatTime" />
      <el-table-column label="文件名" prop="filename" show-overflow-tooltip="true"  />
      <el-table-column label="文件大小" prop="size" />
      <el-table-column align="right">
        <template #header>
          <el-input v-model="search" size="large" placeholder="请输入文件名" />
        </template>
        <!--        <template #default="scope">-->
        <template v-slot="scope">
          <el-button size="large" @click="down(scope.row.filename, scope.row.path)" type="success" v-if="scope.row.type === 1"
          ><el-icon><Files /></el-icon>下载</el-button
          >
          <el-button size="large" @click="open(scope.row.filename)" type="primary" v-if="scope.row.type === 2"><el-icon><Folder /></el-icon>打开</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!--    创建文件夹-->
    <el-dialog v-model="dialogFormVisible" title="创建文件夹" :modal="false" width="40%">
      <el-form :model="state.form" :rules="state.rules" ref="ruleFormRef" style="width: 85%" label-width="120px">
        <el-form-item label="文件名"  prop="status">
          <el-input  v-model="state.form.filename" autocomplete="off" placeholder="请输入文件夹名称"/>
        </el-form-item>
        <!--        <el-form-item label="文件路径"  prop="status">-->
        <!--          <el-input  v-model="state.form.path" autocomplete="off" placeholder="如 /home/userfile/file"/>-->
        <!--        </el-form-item>-->
      </el-form>
      <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取消</el-button>
        <el-button type="primary" @click="save">
          创建
        </el-button>
      </span>
      </template>
    </el-dialog>

  </div>
</template>

<script lang="ts" setup>
import {computed, onMounted, reactive, ref, getCurrentInstance } from 'vue'
import {Folder, CirclePlus,House,DocumentCopy,FolderAdd,Files} from '@element-plus/icons-vue'
import {ElNotification} from "element-plus";
import request from "../request.js";
import router from "../router/index.js";

const ip = getCurrentInstance()?.appContext.config.globalProperties.$ip + "/upload"

// interface User {
//   date: string
//   name: string
//   address: string
// }
interface Files{
  creatTime: string
  filename: string
  size: string
}
const state = reactive({
  form:{},
  down:{},
  // nextPath:"/", ///opt/file
  // upPath:"/",
  path:sessionStorage.getItem('path'),
  nextPath:sessionStorage.getItem('path'), ///opt/file
  upPath:sessionStorage.getItem('path'),
  username:sessionStorage.getItem('username'),
  temp:"",
})

const currentPage = ref(1)
const pageSize = ref(1)
const total = ref(0)
const search = ref('')

const dialogFormVisible = ref(false)
const dialogVisible = ref(false)
const dia = ref(false)
const name = ref(null)

const addfold = () => {
  state.form.path = state.nextPath //初始化数据
  dialogFormVisible.value = true
}
const tableData = reactive({
  Files:{}
})




const load = () => {
  if(state.username == null){
    console.log("name "+state.username + "ip"+ ip)
    ElNotification.error("请先登录")
    router.push("/")
  } else {
    console.log(state.username, state.nextPath, ip)
    request.get("/openFolder", {
      params: {
        path: state.path
      }
    }).then(res=>{
      if(res==false){
        ElNotification.error("文件列表中包含空格")
      }else{
        tableData.Files = res;
        console.log(tableData.Files)
      }
    })
  }
}
load()
const filterTableData = computed(() =>
    tableData.Files.filter(
        (data) =>
            !search.value ||
            data.filename.toLowerCase().includes(search.value.toLowerCase())
    )
)


// const handleEdit = (index: number, row: User) => {
//   console.log(index, row)
// }

const handleSizeChange = (val) => {
  pageSize.value = val
}
const handleCurrentChange = (val) => {
  currentPage.value = val
}
const handleUploadSuccess = (res) => {
  console.log(res)
  if(res == true){
    ElNotification.success("上传成功")
    //不返回首页
    request.get("/openFolder", {
      params:{
        path: state.nextPath
      }
    }).then(res=>{
      if(res==false){
        ElNotification.error("文件列表包含空格无法打开")
      }else {
        tableData.Files = res;
      }
      //console.log(tableData.Files)
    })
  }else {
    ElNotification.error("上传失败注意文件不可有空格!")
  }
}
// const fuGai = () => {
//
// }

const handleAddFile = (filename) => {
  //name.value = filename
  state.down.filename = filename
  dialogVisible.value=true
}

const save = () => {
  state.form.path = state.nextPath
  request.put("/addFolder", state.form).then(res=>{
    if(res == true){
      ElNotification.success("创建成功")
      //load()
      //不返回首页
      request.get("/openFolder", {
        params:{
          path: state.nextPath
        }
      }).then(res=>{
        tableData.Files = res;
        //console.log(tableData.Files)
      })

    }else ElNotification.error("创建失败,同名,有空格或权限不够")
  })
  dialogFormVisible.value = false
}

const open = (foldername) => {
  state.temp = state.nextPath
  //console.log("temp0 "+state.temp)
  state.nextPath =state.nextPath+"/"+foldername;
  //console.log(state.nextPath)
  request.get("/openFolder", {
    params:{
      path: state.nextPath
    }
  }).then(res=>{
      tableData.Files = res;

    //console.log("temp 1"+state.temp+" "+res)
    //console.log("res "+tableData.Files)
  }).catch(onerror=>{
    state.nextPath = state.temp
    ElNotification.error("文件列表有误,查询超时!!!")
    //console.log("res2")
  })
}
const down = (filename, path) => {
  request.get("/download", {
    responseType: 'blob',
    params:{
      file: filename,
      path: path
    }
  }).then(res=>{
    console.log(res)
    /* global window */
    const url = window.URL.createObjectURL(new Blob([res]));
    const link = document.createElement('a');
    link.href = url;
    link.setAttribute('download', filename);
    document.body.appendChild(link);
    link.click();
    ElNotification.success("下载开始,请检查浏览器下载列表")
  }).catch(err=>{
    ElNotification.error(err + " cuow")
  })
}

const upStep = () => {
  //console.log("点击返回 ")
  request.get("/openFolder", {
    params:{
      path: state.upPath
    }
  }).then(res=>{
    tableData.Files = res;
    state.nextPath=state.upPath
    //console.log(tableData.Files)
  }).catch(err=>{
    ElNotification.error("网络拥堵")
  })
}

</script>
<style>
.child {
  position:absolute;
  left:50%;
  transform:translateX(-50%);
}
.parent {
  position:relative;
}
/**修改全局的滚动条*/
/**滚动条的宽度*/

.el-scrollbar__thumb{
  background-color: red;
  width: max-content;
}

</style>

request.js

import axios from 'axios'
import {ElNotification} from "element-plus";
import {getCurrentInstance} from "vue";


const request = axios.create({
    //baseURL: 'http://192.168.188.129:8777',
    //baseURL: 'http://192.168.36.14:8777',
    baseURL: 'http://localhost:8777',
    timeout: 5000,
    headers:{
    },
})


// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
    //console.log("aaa "+this.$ip)
    config.headers['Content-Type'] = 'application/json;charset=utf-8';

    // config.headers['token'] = user.token;  // 设置请求头
    return config
}, error => {
    return Promise.reject(error)
});

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
    response => {
        let res = response.data;
        // 如果是返回的文件
        if (response.config.responseType === 'blob') {
            return res
        }
        // 兼容服务端返回的字符串数据
        if (typeof res === 'string') {
            res = res ? JSON.parse(res) : res
        }
        return res;
    },
    error => {
        //console.log('err' + error) // for debug
        //console.log(1)
        if(error.response.status!==200){
            ElNotification({
                type:'error',
                message: '失败'
            })
        }

        return Promise.reject(error)
    }
)


export default request

后端代码

yml  remoteserver 中填写远端服务器的ip(新版可以在登录界面填写ip),登录用户与密码,以及端口(默认22)

spring:
  freemarker:
    template-loader-path: classpath:/webapp/
    suffix: .html
    charset: utf-8
    cache: false
    expose-request-attributes: true
  resources:
    static-locations: classpath:/static/
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 500MB
server:
  port: 8777
  #address: 192.168.36.14
  address: 0.0.0.0
remoteserver:
  host: 192.168.188.129
  port: 22
  username: root
  password: 123456
  path: "/data"

SftpConnectConfig

package com.gjq.springboot.config;

import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.util.Properties;

/**
 * Sftp配置
 */
@Configuration
@Slf4j
@PropertySource("classpath:application.yml")
public class SftpConnectConfig {

    /**
     * FTP 登录用户名
     */
    @Value("${remoteserver.username}")
//    private String username = "root";
    private String username;
    /**
     * FTP 登录密码
     */
    @Value("${remoteserver.password}")
//    private String password = "123456";
    private String password;

    /**
     * FTP 服务器地址IP地址
     */
    @Value("${remoteserver.host}")
//    private String host = "192.168.188.129";
    private String host;

    /**
     * FTP 端口
     */
    @Value("${remoteserver.port}")
//    private String strPort = "22";
    private String strPort;

    private Session getSession() throws JSchException {
        JSch jsch = new JSch();
        int port = Integer.parseInt(strPort.trim());
        Session session = jsch.getSession(username, host, port);
        if (password != null) {
            session.setPassword(password);
        }
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        // JSch登录sftp,跳过 Kerberos username 身份验证提示
        config.put("PreferredAuthentications","publickey,keyboard-interactive,password");
        session.setConfig(config);
        session.connect();
        return session;
    }

    /**
     * 连接sftp服务器,返回的是sftp连接通道,用来操纵文件
     * @throws Exception
     */
    @Bean
    public ChannelSftp channelSftp() {
        
        return sftp;
    }

    /**
     * 连接sftp服务器,返回exec连接通道,可以远程执行命令
     * @throws Exception
     */
    @Bean
    public ChannelExec channelExec(){
        
        return sftp;
    }
}

SftpFileServer

package com.gjq.springboot.server;

import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.*;
import java.util.*;


@Service
@Slf4j
@PropertySource("classpath:application.yml")
public class SftpFileService {

    @Resource
    private ChannelSftp channelSftp;

    //打开路径
    @Value("${remoteserver.path}")
    private  String FTP_BASEPATH;

    /**
     * 从服务器获取文件并返回字节数组
     * @param path 要下载文件的路径
     * @param file 要下载的文件
     */
    public byte[] download(String path, String file) throws Exception {
        // 切换到文件所在目录
        channelSftp.cd(path);
        //获取文件并返回给输入流,若文件不存在该方法会抛出常
        InputStream is = channelSftp.get(file);
        //ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] fileData = IOUtils.toByteArray(is);
        if(is != null){
            is.close();
        }
        return fileData;
    }

    
    //新建文件夹
    public  boolean addFolder(String filename, String path){
        
        return false;
    }
    //重名文件处理
    public static String addWei(List<com.gjq.springboot.entity.File> ftpFiles, String originFileName){
        for (com.gjq.springboot.entity.File ftpFile : ftpFiles){
            if(originFileName.equals(ftpFile.getFilename())){
                //System.out.println("3 "+originFileName);
                //同名文件 递归
                String filename = ftpFile.getFilename();
                String suffix = filename.substring(filename.lastIndexOf("."));
                String forth = filename.substring(0,filename.lastIndexOf("."));
                if(forth.length()<4){
                    originFileName = forth +"(1)"+suffix;
                    return addWei(ftpFiles, originFileName);
                }else{
                    String wei = forth.substring(forth.length()-3, forth.length());
                    String qian = forth.substring(0, forth.length()-3);
                    //System.out.println(wei);
                    //System.out.println(qian);
                    if(wei.charAt(0) == '(' && wei.charAt(2) == ')'){
                        int time = Integer.valueOf(wei.charAt(1)) - 48;
                        System.out.println("time"+time);
                        time++;
                        wei = "(" + time +")";
                        originFileName = qian + wei +suffix;
                        System.out.println("2 "+originFileName);
                        return addWei(ftpFiles, originFileName);
                    }else{
                        originFileName = forth +"(1)"+suffix;
                        System.out.println("(1)");
                        return addWei(ftpFiles, originFileName);
                    }
                }
            }
        }
        //System.out.println("final_qian "+originFileName);
        return originFileName;
    }
}

SftpFileController

package com.gjq.springboot.controller;



import com.gjq.springboot.entity.File;
import com.gjq.springboot.entity.downDTO;
import com.gjq.springboot.utils.FtpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import com.gjq.springboot.server.SftpFileService;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;



@RestController
@Slf4j
@CrossOrigin//该注解可以解决跨越问题。
public class SftpFileController {


    @Autowired
    private SftpFileService fileService;

//    @GetMapping("/download")
//    public ResponseEntity<byte[]> download(@RequestParam("file") String file, @RequestParam("path")String path,
//                                           HttpServletResponse response){
//        System.out.println(file + " " + path);
//        //设置响应信息
//        response.setContentType("application/octet-stream");
//        // filename为文件下载后保存的文件名,可自行设置,但是注意文件名后缀,要和原来的保持一致
//        response.setHeader("Content-Disposition", "attachment; filename=" + file);
//        //OutputStream out = null;
//        HttpHeaders headers = new HttpHeaders();
//        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
//        headers.setContentDispositionFormData("attachment", file);
//        try {
//            //out = response.getOutputStream();
//            // 输出到客户端
//            //out.write(fileService.download(path, file));
//            if (fileService.download(path, file) == null) {
//                return new ResponseEntity<>(HttpStatus.NOT_FOUND);
//            }
//            return new ResponseEntity<>(fileService.download(path, file), headers, HttpStatus.OK);
//        } catch (Exception e) {
//            log.error("",e);
//            return null;
//        }
//    }

    /**
     * 通过浏览器下载文件
     * @param file 文件名
     * @param path 文件在服务器的路基
     * @param response
     * @return
     */
    @GetMapping("/download")
    public ResponseEntity<byte[]> download(@RequestParam("file") String file, @RequestParam("path")String path,
                                           HttpServletResponse response) {
        
        return new ResponseEntity<>(fileContent, headers, HttpStatus.OK);
    }

    /**
     * 上传文件到服务器
     * @param multipartFile 要上传到服务器的文件,注意此处的path必须在结尾添加 /
     * @param path 上传到服务器的路径
     */
    @PostMapping("/upload")
    public boolean upload(@RequestParam("file") MultipartFile multipartFile, @RequestParam("selectIndex") String path){
        
            return false;
        }
    }

    @GetMapping("/filesdatalistplatform")
    public Object getFilesList(String path){

        return fileService.getFileList(path);
    }

    @PutMapping("/addFolder")
    @ResponseBody
    public boolean addFolder(@RequestBody File file){
       
        return fileService.addFolder(file.getFilename(), path);
    }

    @GetMapping("/openFolder")
    public Object open(@RequestParam("path") String path){
       
        return fileService.getFileList(path);
    }
}

3.运行截图

f1281d098e604d62b9841a1e4616b29f.png

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值