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);
}
}