文件分享
文件分享实现过程概览
-
用户可以将自己的文件(
单个文件或单个文件夹
的方式)以
链接 + 提取码
的形式分享出去,其它用户(包括分享该文件的用户)都需要先输入提取码,才能够查看用户分享的文件 -
获取分享文件的过程:用户输入用户分享的链接,跳转到
文件分享的页面
,当前页面会立即发请求给后台,求证
当前会话是否该分享输入正确过提取码
,如果输入过,则正常获取该分享的文件列表信息。如果没有输入过,那么,让用户跳转到
输入提取码的页面
,然后,发请求给后台,验证提取码是否正确,如果正确,则跳转到文件分享页面。 - 用户来到文件分享页面,可以预览用户分享的文件、保存用户分享的文件(可单个保存、多个保存)
- 当前用户所分享的文件在查看文件分享页面时,可以取消分享(整个分享全部取消分享)
将文件分享出去
ShareFile.vue
- Object.assign({},data) 完成赋值
- 复制 分享链接及提取码 (使用vue-clipboard3实现)
-
可以通过
window.location.origin
获取当前的域,可参考:
浏览器中location详解
<template>
<div>
<Dialog
:show="dialogConfig.show"
:title="dialogConfig.title"
:buttons="dialogConfig.buttons"
width="600px"
:showCancel="showCancel"
@close="dialogConfig.show = false"
>
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
label-width="100px"
@submit.prevent
>
<el-form-item label="文件"> {{ formData.fileName }} </el-form-item>
<template v-if="showType == 0">
<el-form-item label="有效期" prop="validType">
<el-radio-group v-model="formData.validType">
<el-radio :label="0">1天</el-radio>
<el-radio :label="1">7天</el-radio>
<el-radio :label="2">30天</el-radio>
<el-radio :label="3">永久有效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="提取码" prop="codeType">
<el-radio-group v-model="formData.codeType">
<el-radio :label="0">自定义</el-radio>
<el-radio :label="1">系统生成</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="code" v-if="formData.codeType == 0">
<el-input
clearable
placeholder="请输入5位提取码"
v-model.trim="formData.code"
maxLength="5"
:style="{ width: '130px' }"
></el-input>
</el-form-item>
</template>
<template v-else>
<el-form-item label="分享连接" prop="validType">
{{ shareUrl }}{{ resultInfo.shareId }}
</el-form-item>
<el-form-item label="提取码" prop="validType">
{{ resultInfo.code }}
</el-form-item>
<el-form-item prop="validType">
<el-button type="primary" @click="copy">复制链接极提取码</el-button>
</el-form-item>
</template>
</el-form>
</Dialog>
</div>
</template>
<script setup>
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, getCurrentInstance, nextTick } from "vue";
import { useRouter } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
// 分享链接的前缀(document.location.origin获取当前页面来源的域名的标准形式)(浏览器中location详解)[https://blog.csdn.net/qq_42880714/article/details/126167569]
const shareUrl = ref(document.location.origin + "/share/");
const api = {
shareFile: "/share/shareFile",
};
// 分享之前的表单 - 0 / 分享之后的表单 - 1
const showType = ref(0);
// 分享表单数据
const formData = ref({});
// 分享表单引用
const formDataRef = ref();
// 表单校验规则
const rules = {
validType: [{ required: true, message: "请选择有效期" }],
codeType: [{ required: true, message: "请选择提取码类型" }],
code: [
{ required: true, message: "请输入提取码" },
{ validator: proxy.Verify.shareCode, message: "提取码只能是数字字母" },
{ min: 5, message: "提取码最少5位" },
],
};
// 弹框是否显示取消按钮
const showCancel = ref(true);
// 弹框配置
const dialogConfig = ref({
show: false,
title: "分享",
buttons: [
{
type: "primary",
text: "确定",
click: (e) => {
share();
},
},
],
});
const resultInfo = ref({});
// 确定分享该文件
const share = async () => {
// 当再次点击时, 关闭弹框
if (Object.keys(resultInfo.value).length > 0) {
dialogConfig.value.show = false;
return;
}
// 校验表单
formDataRef.value.validate(async (valid) => {
// 校验不通过
if (!valid) {
return;
}
// 下面2句, 可以直接写成 let params = Object.assign({}, formData.value)
let params = {};
Object.assign(params, formData.value);
let result = await proxy.Request({
url: api.shareFile,
params: params,
});
if (!result) {
return;
}
// 标记已分享
showType.value = 1;
// 文件分享的信息
resultInfo.value = result.data;
// 将确定按钮改为关闭
dialogConfig.value.buttons[0].text = "关闭";
// 不显示弹框的取消按钮
showCancel.value = false;
});
};
// 展示分享弹框
const show = (data) => {
// 弹框显示取消按钮
showCancel.value = true;
// 显示弹框
dialogConfig.value.show = true;
// 标记在未实际分享之前
showType.value = 0;
// 分享后的数据信息
resultInfo.value = {};
nextTick(() => {
// 重置表单
formDataRef.value.resetFields();
// 将表格的数据, 复制给 分享表单
formData.value = Object.assign({}, data);
});
};
// 将show方法暴露出去
defineExpose({ show });
// 复制功能实现
const copy = async () => {
await toClipboard(
`链接:${shareUrl.value}${resultInfo.value.shareId} 提取码: ${resultInfo.value.code}`
);
proxy.Message.success("复制成功");
};
</script>
<style lang="scss" scoped></style>
用户已分享文件列表
Share.vue
-
使用@import引入css,如:
@import "@/assets/file.list.scss";
- vue-clipboard3实现文本内容复制
<template>
<div>
<div class="top">
<el-button
type="primary"
:disabled="selectIdList.length == 0"
@click="cancelShareBatch">
<span class="iconfont icon-cancel"></span>取消分享
</el-button>
</div>
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:options="tableOptions"
@rowSelected="rowSelected"
>
<template #fileName="{ index, row }">
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)">
<template v-if="(row.fileType == 3 || row.fileType == 1) && row.status !== 0 ">
<icon :cover="row.fileCover"></icon>
</template>
<template v-else>
<icon v-if="row.folderType == 0" :fileType="row.fileType"></icon>
<icon v-if="row.folderType == 1" :fileType="0"></icon>
</template>
<span
class="file-name"
v-if="!row.showRename"
:title="row.fileName">
<span>{{ row.fileName }}</span>
</span>
<span class="op">
<template v-if="row.showOp && row.fileId">
<span class="iconfont icon-link" @click="copy(row)">复制链接</span>
<span class="iconfont icon-cancel" @click="cancelShare(row)">取消分享</span>
</template>
</span>
</div>
</template>
<template #expireTime="{ index, row }">
{{ row.validType == 3 ? "永久" : row.expireTime }}
</template>
</Table>
</div>
</div>
</template>
<script setup>
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();
import { ref, reactive, getCurrentInstance, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
loadDataList: "/share/loadShareList",
cancelShare: "/share/cancelShare",
};
const shareUrl = ref(document.location.origin + "/share/");
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "分享时间",
prop: "shareTime",
width: 200,
},
{
label: "失效时间",
prop: "expireTime",
scopedSlots: "expireTime",
width: 200,
},
{
label: "浏览次数",
prop: "showCount",
width: 200,
},
];
//搜索
const search = () => {
showLoading.value = true;
loadDataList();
};
//列表
const tableData = ref({});
const tableOptions = {
extHeight: 20,
selectType: "checkbox",
};
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
};
if (params.category !== "all") {
delete params.filePid;
}
let result = await proxy.Request({
url: api.loadDataList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
//展示操作按钮
const showOp = (row) => {
tableData.value.list.forEach((element) => {
element.showOp = false;
});
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
//复制链接
const copy = async (data) => {
await toClipboard(`链接:${shareUrl.value}${data.shareId} 提取码: ${data.code}`);
proxy.Message.success("复制成功");
};
//多选 批量选择
const selectIdList = ref([]);
const rowSelected = (rows) => {
selectIdList.value = [];
rows.forEach((item) => {
selectIdList.value.push(item.shareId);
});
};
//取消分享
const cancelShareIdList = ref([]);
const cancelShareBatch = () => {
if (selectIdList.value.length == 0) {
return;
}
cancelShareIdList.value = selectIdList.value;
cancelShareDone();
};
const cancelShare = (row) => {
cancelShareIdList.value = [row.shareId];
cancelShareDone();
};
const cancelShareDone = async () => {
proxy.Confirm(`你确定要取消分享吗?`, async () => {
let result = await proxy.Request({
url: api.cancelShare,
params: {
shareIds: cancelShareIdList.value.join(","),
},
});
if (!result) {
return;
}
proxy.Message.success("取消分享成功");
loadDataList();
});
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.file-list {
margin-top: 10px;
.file-item {
.file-name {
span {
&:hover {
color: #494944;
}
}
}
.op {
width: 170px;
}
}
}
</style>
用户输入分享链接,进入验证页面
- 其它用户拿到链接之后,将分享链接输入到地址栏,但是,用户第一次输入的话,肯定没有验证过(会话中,没有此分享记录),因此,要跳转到分享验证页面,验证通过之后,该分享记录会存入此会话,当用户再一次进行此分享页面,就会从会话中检测到有此分享记录,此时就不需要再次验证了。
ShareCheck.vue
-
背景图片设置,可参考:
CSS背景background设置
<template>
<div class="share">
<div class="body-content">
<div class="logo">
<span class="iconfont icon-pan"></span>
<span class="name">Easy云盘</span>
</div>
<div class="code-panel">
<div class="file-info">
<div class="avatar">
<Avatar
:userId="shareInfo.userId"
:avatar="shareInfo.avatar"
:width="50"
></Avatar>
</div>
<div class="share-info">
<div class="user-info">
<span class="nick-name">{{ shareInfo.nickName }} </span>
<span class="share-time">分享于 {{ shareInfo.shareTime }}</span>
</div>
<div class="file-name">分享文件:{{ shareInfo.fileName }}</div>
</div>
</div>
<div class="code-body">
<div class="tips">请输入提取码:</div>
<div class="input-area">
<el-form
:model="formData"
:rules="rules"
ref="formDataRef"
:maxLength="5"
@submit.prevent
>
<!--input输入-->
<el-form-item prop="code">
<el-input
class="input"
v-model="formData.code"
@keyup.enter="checkShare"
></el-input>
<el-button type="primary" @click="checkShare"
>提取文件</el-button
>
</el-form-item>
</el-form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, nextTick, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
getShareInfo: "/showShare/getShareInfo",
checkShareCode: "/showShare/checkShareCode",
};
const shareId = route.params.shareId;
const shareInfo = ref({});
const getShareInfo = async () => {
let result = await proxy.Request({
url: api.getShareInfo,
params: {
shareId,
},
});
if (!result) {
return;
}
shareInfo.value = result.data;
};
getShareInfo();
const formData = ref({});
const formDataRef = ref();
const rules = {
code: [
{ required: true, message: "请输入提取码" },
{ min: 5, message: "提取码为5位" },
{ max: 5, message: "提取码为5位" },
],
};
const checkShare = async () => {
formDataRef.value.validate(async (valid) => {
if (!valid) {
return;
}
let result = await proxy.Request({
url: api.checkShareCode,
params: {
shareId: shareId,
code: formData.value.code,
},
});
if (!result) {
return;
}
router.push(`/share/${shareId}`);
});
};
</script>
<style lang="scss" scoped>
.share {
height: calc(100vh);
background: url("../../assets/share_bg.png");
background-repeat: repeat-x;
background-position: 0 bottom;
background-color: #eef2f6;
display: flex;
justify-content: center;
.body-content {
margin-top: calc(100vh / 5);
width: 500px;
.logo {
display: flex;
align-items: center;
justify-content: center;
.icon-pan {
font-size: 60px;
color: #409eff;
}
.name {
font-weight: bold;
margin-left: 5px;
font-size: 25px;
color: #409eff;
}
}
.code-panel {
margin-top: 20px;
background: #fff;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 0 7px 1px #5757574f;
.file-info {
padding: 10px 20px;
background: #409eff;
color: #fff;
display: flex;
align-items: center;
.avatar {
margin-right: 5px;
}
.share-info {
.user-info {
display: flex;
align-items: center;
.nick-name {
font-size: 15px;
}
.share-time {
margin-left: 20px;
font-size: 12px;
}
}
.file-name {
margin-top: 10px;
font-size: 12px;
}
}
}
.code-body {
padding: 30px 20px 60px 20px;
.tips {
font-weight: bold;
}
.input-area {
margin-top: 10px;
.input {
flex: 1;
margin-right: 10px;
}
}
}
}
}
}
</style>
验证成功后,进入分享页面
- 进入分享页面,首先仍然要请求接口,判断当前会话是否已经输入当前分享的提取码,如果没有输入,则跳转到验证页面
Share.vue
<template>
<div class="share">
<div class="header">
<div class="header-content">
<div class="logo" @click="jump">
<span class="iconfont icon-pan"></span>
<span class="name">Easy云盘</span>
</div>
</div>
</div>
<div class="share-body">
<template v-if="Object.keys(shareInfo).length == 0">
<div
v-loading="Object.keys(shareInfo).length == 0"
class="loading"
></div>
</template>
<template v-else>
<div class="share-panel">
<div class="share-user-info">
<div class="avatar">
<Avatar
:userId="shareInfo.userId"
:avatar="shareInfo.avatar"
:width="50"
></Avatar>
</div>
<div class="share-info">
<div class="user-info">
<span class="nick-name">{{ shareInfo.nickName }} </span>
<span class="share-time">分享于 {{ shareInfo.shareTime }}</span>
</div>
<div class="file-name">分享文件:{{ shareInfo.fileName }}</div>
</div>
</div>
<div class="share-op-btn">
<el-button
type="primary"
v-if="shareInfo.currentUser"
@click="cancelShare"
><span class="iconfont icon-cancel"></span>取消分享</el-button
>
<el-button
v-else
type="primary"
:disabled="selectFileIdList.length == 0"
@click="save2MyPan"
><span class="iconfont icon-import"></span
>保存到我的网盘</el-button
>
</div>
</div>
<!--导航-->
<Navigation
ref="navigationRef"
@navChange="navChange"
:shareId="shareId"
></Navigation>
<div class="file-list">
<Table
:columns="columns"
:showPagination="true"
:dataSource="tableData"
:fetch="loadDataList"
:initFetch="false"
:options="tableOptions"
:showPageSize="false"
@rowSelected="rowSelected"
>
<template #fileName="{ index, row }">
<div
class="file-item"
@mouseenter="showOp(row)"
@mouseleave="cancelShowOp(row)"
>
<template
v-if="
(row.fileType == 3 || row.fileType == 1) && row.status !== 0
"
>
<icon :cover="row.fileCover"></icon>
</template>
<template v-else>
<icon
v-if="row.folderType == 0"
:fileType="row.fileType"
></icon>
<icon v-if="row.folderType == 1" :fileType="0"></icon>
</template>
<span class="file-name" :title="row.fileName">
<span @click="preview(row)">{{ row.fileName }}</span>
</span>
<span class="op">
<span
v-if="row.folderType == 0"
class="iconfont icon-download"
@click="download(row.fileId)"
>下载</span
>
<template v-if="row.showOp && !shareInfo.currentUser">
<span
class="iconfont icon-import"
@click="save2MyPanSingle(row)"
>保存到我的网盘</span
>
</template>
</span>
</div>
</template>
<template #fileSize="{ index, row }">
<span v-if="row.fileSize">
{{ proxy.Utils.sizeToStr(row.fileSize) }}</span
>
</template>
</Table>
</div>
</template>
<!--选择目录-->
<FolderSelect
ref="folderSelectRef"
@folderSelect="save2MyPanDone"
></FolderSelect>
<!--预览-->
<Preview ref="previewRef"> </Preview>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance, watch } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
const api = {
getShareLoginInfo: "/showShare/getShareLoginInfo",
loadFileList: "/showShare/loadFileList",
createDownloadUrl: "/showShare/createDownloadUrl",
download: "/api/showShare/download",
cancelShare: "/share/cancelShare",
saveShare: "/showShare/saveShare",
};
const shareId = route.params.shareId;
const shareInfo = ref({});
const getShareInfo = async () => {
let result = await proxy.Request({
url: api.getShareLoginInfo,
showLoading: false,
params: {
shareId,
},
});
if (!result) {
return;
}
if (result.data == null) {
router.push("/shareCheck/" + shareId);
return;
}
shareInfo.value = result.data;
};
getShareInfo();
//列表
const columns = [
{
label: "文件名",
prop: "fileName",
scopedSlots: "fileName",
},
{
label: "修改时间",
prop: "lastUpdateTime",
width: 200,
},
{
label: "大小",
prop: "fileSize",
scopedSlots: "fileSize",
width: 200,
},
];
const tableData = ref({});
const tableOptions = {
extHeight: 80,
selectType: "checkbox",
};
const loadDataList = async () => {
let params = {
pageNo: tableData.value.pageNo,
pageSize: tableData.value.pageSize,
shareId: shareId,
filePid: currentFolder.value.fileId,
};
let result = await proxy.Request({
url: api.loadFileList,
params,
});
if (!result) {
return;
}
tableData.value = result.data;
};
//展示操作按钮
const showOp = (row) => {
tableData.value.list.forEach((element) => {
element.showOp = false;
});
row.showOp = true;
};
const cancelShowOp = (row) => {
row.showOp = false;
};
//多选 批量选择
const selectFileIdList = ref([]);
const rowSelected = (rows) => {
selectFileIdList.value = [];
rows.forEach((item) => {
selectFileIdList.value.push(item.fileId);
});
};
//目录
const currentFolder = ref({ fileId: 0 });
const navChange = (data) => {
const { curFolder } = data;
currentFolder.value = curFolder;
loadDataList();
};
//查看
const previewRef = ref();
const navigationRef = ref();
const preview = (data) => {
if (data.folderType == 1) {
navigationRef.value.openFolder(data);
return;
}
data.shareId = shareId;
previewRef.value.showPreview(data, 2);
};
//下载文件
const download = async (fileId) => {
let result = await proxy.Request({
url: api.createDownloadUrl + "/" + shareId + "/" + fileId,
});
if (!result) {
return;
}
window.location.href = api.download + "/" + result.data;
};
//保存到我的网盘
const folderSelectRef = ref();
const save2MyPanFileIdArray = [];
const save2MyPan = () => {
if (selectFileIdList.value.length == 0) {
return;
}
if (!proxy.VueCookies.get("userInfo")) {
router.push("/login?redirectUrl=" + route.path);
return;
}
save2MyPanFileIdArray.values = selectFileIdList.value;
folderSelectRef.value.showFolderDialog();
};
const save2MyPanSingle = (row) => {
if (!proxy.VueCookies.get("userInfo")) {
router.push("/login?redirectUrl=" + route.path);
return;
}
save2MyPanFileIdArray.values = [row.fileId];
folderSelectRef.value.showFolderDialog();
};
//执行保存操作
const save2MyPanDone = async (folderId) => {
let result = await proxy.Request({
url: api.saveShare,
params: {
shareId: shareId,
shareFileIds: save2MyPanFileIdArray.values.join(","),
myFolderId: folderId,
},
});
if (!result) {
return;
}
loadDataList();
proxy.Message.success("保存成功");
folderSelectRef.value.close();
};
//取消分享
const cancelShare = () => {
proxy.Confirm(`你确定要取消分享吗?`, async () => {
let result = await proxy.Request({
url: api.cancelShare,
params: {
shareIds: shareId,
},
});
if (!result) {
return;
}
proxy.Message.success("取消分享成功");
router.push("/");
});
};
const jump = () => {
router.push("/");
};
</script>
<style lang="scss" scoped>
@import "@/assets/file.list.scss";
.header {
width: 100%;
position: fixed;
background: #0c95f7;
height: 50px;
.header-content {
width: 70%;
margin: 0px auto;
color: #fff;
line-height: 50px;
.logo {
display: flex;
align-items: center;
cursor: pointer;
.icon-pan {
font-size: 40px;
}
.name {
font-weight: bold;
margin-left: 5px;
font-size: 25px;
}
}
}
}
.share-body {
width: 70%;
margin: 0px auto;
padding-top: 50px;
.loading {
height: calc(100vh / 2);
width: 100%;
}
.share-panel {
margin-top: 20px;
display: flex;
justify-content: space-around;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
.share-user-info {
flex: 1;
display: flex;
align-items: center;
.avatar {
margin-right: 5px;
}
.share-info {
.user-info {
display: flex;
align-items: center;
.nick-name {
font-size: 15px;
}
.share-time {
margin-left: 20px;
font-size: 12px;
}
}
.file-name {
margin-top: 10px;
font-size: 12px;
}
}
}
}
}
.file-list {
margin-top: 10px;
.file-item {
.op {
width: 170px;
}
}
}
</style>
WebShareController#loadFileList接口
- 当用户验证成功之后,来到分享页面,需要查询到当前分享的文件fileId,这个fileId可能是一个文件,也可能是一个文件夹。如果是个文件夹,当进入这个文件夹时,此时导航也要进入此层级,导航进入后,根据路由path获取导航信息,然后,通知父组件加载这个点击的文件夹的文件和文件夹。父组件则需要以这个文件夹为filePid查询这个filePid下的下一级文件和文件夹,注意此时的后端,不能直接把前端传过来的filePid直接使用,因为这个接口是开放的(即使用户不登录,也可以访问),所以需要对这个filePid做校验,需要验证这个filePid是这个分享关联的fileId(或这个fileId的子级文件夹),以避免数据安全问题。
@RestController("webShareController")
@RequestMapping("/showShare")
public class WebShareController extends CommonFileController {
/**
* 获取文件列表
*
* @param session
* @param shareId
* @return
*/
@RequestMapping("/loadFileList")
@GlobalInterceptor(checkLogin = false, checkParams = true)
public ResponseVO loadFileList(HttpSession session,
@VerifyParam(required = true) String shareId, String filePid) {
// 根据 当前用户是否 输入过提取码,以及校验当前分享是否已失效
SessionShareDto shareSessionDto = checkShare(session, shareId);
FileInfoQuery query = new FileInfoQuery();
// filePid不为0, 则需要校验。因为要校验进入这个分享所关联的fileId及这个fileId下的所有文件夹
// (不能所有fileId都能访问,因此需要递归查询校验)
if (!StringTools.isEmpty(filePid) && !Constants.ZERO_STR.equals(filePid)) {
// 校验当前要查看的分享中的文件夹,是否包含在 分享的文件夹当中
fileInfoService.checkRootFilePid(shareSessionDto.getFileId(), shareSessionDto.getShareUserId(), filePid);
// 设置为filePid
query.setFilePid(filePid);
} else {
query.setFileId(shareSessionDto.getFileId());
}
// 查询文件记录
query.setUserId(shareSessionDto.getShareUserId());
query.setOrderBy("last_update_time desc");
query.setDelFlag(FileDelFlagEnums.USING.getFlag());
PaginationResultVO resultVO = fileInfoService.findListByPage(query);
return getSuccessResponseVO(convert2PaginationVO(resultVO, FileInfoVO.class));
}
}
FileInfoServiceImpl#checkRootFilePid 递归校验
@Service("fileInfoService")
public class FileInfoServiceImpl implements FileInfoService {
@Override
public void checkRootFilePid(String rootFilePid, String userId, String fileId) {
// rootFilePid 是用户分享记录中分享的文件id
// fileId 是用户分享出来的文件中某个文件id
// 这里就是在校验 当前用户正在查看的 fileId 是不是在 分享的根fileId的子级文件中(再怎么样,也不能越过分享的顶级fileId)
if (StringTools.isEmpty(fileId)) {
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
// 如果就是查的顶级fileId, 校验通过
if (rootFilePid.equals(fileId)) {
return;
}
// 递归的去找fileId的父fileId, 一直到 查找到fileId就是 分享的根fileId(能找到) 或者是 到顶级fileId 0(找不到)
checkFilePid(rootFilePid, fileId, userId);
}
private void checkFilePid(String rootFilePid, String fileId, String userId) {
// 一直往上级fileId去找
FileInfo fileInfo = this.fileInfoMapper.selectByFileIdAndUserId(fileId, userId);
if (fileInfo == null) {
// fileId未有记录
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
if (Constants.ZERO_STR.equals(fileInfo.getFilePid())) {
// 一直到顶级,仍未找到
throw new BusinessException(ResponseCodeEnum.CODE_600);
}
if (fileInfo.getFilePid().equals(rootFilePid)) {
// 找到了 返回
return;
}
// 递归的去找
checkFilePid(rootFilePid, fileInfo.getFilePid(), userId);
}
}
WebShareController#saveShare 接口
- 将 其它用户分享的文件,保存到 自己的空间中
@RestController("webShareController")
@RequestMapping("/showShare")
public class WebShareController extends CommonFileController {
// 将 其它用户分享的文件,保存到 自己的空间中
/**
* 保存分享
*
* @param session
* @param shareId
* @param shareFileIds
* @param myFolderId
* @return
*/
@RequestMapping("/saveShare")
@GlobalInterceptor(checkParams = true)
public ResponseVO saveShare(HttpSession session,
@VerifyParam(required = true) String shareId,
@VerifyParam(required = true) String shareFileIds,
@VerifyParam(required = true) String myFolderId) {
SessionShareDto shareSessionDto = checkShare(session, shareId);
SessionWebUserDto webUserDto = getUserInfoFromSession(session);
// 禁止用户把自己分享的文件保存到自己的网盘中(百度网盘中也是不允许的)
if (shareSessionDto.getShareUserId().equals(webUserDto.getUserId())) {
throw new BusinessException("自己分享的文件无法保存到自己的网盘");
}
// 分享的根文件id、当前分享中要保存的fileIds、保存到哪个文件夹中、谁分享的、当前用户
fileInfoService.saveShare(shareSessionDto.getFileId(), shareFileIds, myFolderId, shareSessionDto.getShareUserId(), webUserDto.getUserId());
return getSuccessResponseVO(null);
}
}
FileInfoServiceImpl #saveShare 保存别人分享的文件 递归子文件夹
/**
* 文件信息 业务接口实现
*/
@Service("fileInfoService")
public class FileInfoServiceImpl implements FileInfoService {
@Override
public void saveShare(String shareRootFilePid, String shareFileIds, String myFolderId, String shareUserId, String cureentUserId) {
// 待保存的分享fileId数组
String[] shareFileIdArray = shareFileIds.split(",");
//目标目录文件列表
//(查看保存到当前用户的目标目录里下的所有文件和文件夹,为了后面的重命名)
FileInfoQuery fileInfoQuery = new FileInfoQuery();
fileInfoQuery.setUserId(cureentUserId);
fileInfoQuery.setFilePid(myFolderId);
List<FileInfo> currentFileList = this.fileInfoMapper.selectList(fileInfoQuery);
// list转map,文件名作为key
Map<String, FileInfo> currentFileMap = currentFileList.stream().collect(Collectors.toMap(FileInfo::getFileName, Function.identity(), (file1, file2) -> file2));
//选择的文件
//(用户在所分享的文件中选择要保存的文件集合)
fileInfoQuery = new FileInfoQuery();
fileInfoQuery.setUserId(shareUserId);
fileInfoQuery.setFileIdArray(shareFileIdArray);
List<FileInfo> shareFileList = this.fileInfoMapper.selectList(fileInfoQuery);
// 最终需要将分享的文件 插入 到 当前用户的文件记录 的集合
List<FileInfo> copyFileList = new ArrayList<>();
Date curDate = new Date();
// (遍历所有需要保存的 分享文件)
for (FileInfo item : shareFileList) {
// (在用户需要保存的目录里是否已经存在了 要保存的分享文件的名称)
FileInfo haveFile = currentFileMap.get(item.getFileName());
// (如果已经存在了的话,那就需要重命名当前分享文件的名称)
if (haveFile != null) {
item.setFileName(StringTools.rename(item.getFileName()));
}
// (将当前分享文件 的所有子级文件也拷贝给 当前用户,都暂存到 copyFileList 中)
// (如果 这里,我自己写递归的话,有可能,在当前代码块,就把item添加到了copyFileList,
// 然后,再调用递归方法,在递归方法里面,再判断是不是个文件夹,是文件夹的话,遍历文件夹里的每项,再调用递归方法,
// 这样比起这里来说,有一段添加到copyFileList的逻辑写到当前代码块里了,而没有都在递归里面,不是很好。这里封装的比较好,可以多体会下递归。)
// 应该说是2者对递归的方法定义或者说思路不同:
// 我的对递归的思路是:这个递归方法 可以判断当前的文件是否为文件夹,如果是文件夹的话,那么遍历这个文件夹中的每一项,然后将每一项都添加到结果集合中,然后对每一项再进行递归。如果不是文件夹,则直接添加,不递归。
// 他的思路是:这个递归方法,可以把传入的文件添加到结果集合中,然后判断当前是否为文件夹,如果是文件夹的话,那么遍历这个文件夹的每一项,对每一项再调用递归,如果不是文件夹的话,则略过。
findAllSubFile(copyFileList, item, shareUserId, cureentUserId, curDate, myFolderId);
}
System.out.println(copyFileList.size());
this.fileInfoMapper.insertBatch(copyFileList);
}
private void findAllSubFile(List<FileInfo> copyFileList, FileInfo fileInfo, String sourceUserId, String currentUserId, Date curDate, String newFilePid) {
// 先将当前的文件或者是文件夹 (需要设置一个新的fileId,一个新的filePid,当前用户userId),存入copyFileList
String sourceFileId = fileInfo.getFileId();
fileInfo.setCreateTime(curDate);
fileInfo.setLastUpdateTime(curDate);
fileInfo.setFilePid(newFilePid);
fileInfo.setUserId(currentUserId);
String newFileId = StringTools.getRandomString(Constants.LENGTH_10);
fileInfo.setFileId(newFileId);
copyFileList.add(fileInfo); // 此时,将fileInfo添加到copyFileList中
// (如果当前存入的是一个文件夹,那么需要递归的将这个文件夹下面的所有文件和这个文件夹下面的所有文件夹(包括子文件和子文件夹),都加到存入copyFileList中)
if (FileFolderTypeEnums.FOLDER.getType().equals(fileInfo.getFolderType())) {
FileInfoQuery query = new FileInfoQuery();
query.setFilePid(sourceFileId);
query.setUserId(sourceUserId);
// 查询当前文件夹的所有 直接子文件和直接子文件夹
List<FileInfo> sourceFileList = this.fileInfoMapper.selectList(query);
// 遍历当前文件夹的所有 直接子文件和直接子文件夹
for (FileInfo item : sourceFileList) {
// 递归处理
findAllSubFile(copyFileList, item, sourceUserId, currentUserId, curDate, newFileId);
}
}
}
}
版权声明:本文为qq_16992475原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。