实现小文件在浏览器的秒传功能

This commit is contained in:
LittleBoy 2022-05-10 10:53:33 +08:00
parent 2342f0202e
commit 26a2be3751
14 changed files with 278 additions and 97 deletions

View File

@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import xyz.longicorn.driver.config.BizException;
import xyz.longicorn.driver.dto.ApiResult;
import xyz.longicorn.driver.dto.FastUploadFile;
import xyz.longicorn.driver.pojo.FileInfo;
import xyz.longicorn.driver.pojo.FolderInfo;
import xyz.longicorn.driver.service.FileService;
@ -54,17 +55,11 @@ public class UploadController {
}
fileInfo.setFolderId(folder.getId());
}
fileInfo.setUid(1l);
fileInfo.setUid(1);
fileInfo.setHash(md5);//
fileInfo.setName(file.getOriginalFilename());
fileInfo.setSize(file.getSize());
String type = file.getContentType().toLowerCase();
if (FILE_MIME_TYPE.containsKey(type)) {
type = FILE_MIME_TYPE.get(type);
} else {
type = "file";
}
fileInfo.setType(type);
fileInfo.setType(getType(file.getContentType()));
if (f != null) { // 系统已经存在了该文件
// 不保存上传文件 直接copy数据
fileInfo.setPath(f.getPath());
@ -77,4 +72,23 @@ public class UploadController {
fileService.save(fileInfo);//保存文件信息
return ApiResult.success(null);
}
// 将mimetype -> 文件后缀名的类型
private String getType(String mimeType) {
String type = mimeType.toLowerCase();
if (FILE_MIME_TYPE.containsKey(type)) {
return FILE_MIME_TYPE.get(type);
}
return "file";
}
@PostMapping("/fast-upload")
public ApiResult fastUpload(@RequestBody FastUploadFile file) {
int uid = 1;
file.setType(getType(file.getType()));
FileInfo info = fileService.fastUpload(uid, file);
return ApiResult.success(info);
}
}

View File

@ -2,8 +2,10 @@ package xyz.longicorn.driver.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xyz.longicorn.driver.pojo.FolderInfo;
@Mapper
public interface FolderInfoMapper extends BaseMapper<FolderInfo> {
public FolderInfo getByPath(@Param("uid") int uid, @Param("path") String path);
}

View File

@ -0,0 +1,12 @@
package xyz.longicorn.driver.dto;
import lombok.Data;
@Data
public class FastUploadFile {
private String name;
private Long size;
private String type;
private String hash;
private String folder;
}

View File

@ -19,7 +19,7 @@ import java.util.Date;
@Accessors(chain = true)
public class FileInfo implements Serializable {
private Long id;
private Long uid;
private Integer uid;
private String name;
private String hash;
private Long folderId;

View File

@ -3,13 +3,21 @@ package xyz.longicorn.driver.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import xyz.longicorn.driver.config.BizException;
import xyz.longicorn.driver.dao.FileInfoMapper;
import xyz.longicorn.driver.dao.FolderInfoMapper;
import xyz.longicorn.driver.dto.FastUploadFile;
import xyz.longicorn.driver.pojo.FileInfo;
import xyz.longicorn.driver.pojo.FolderInfo;
import javax.annotation.Resource;
import java.util.List;
@Service
public class FileService extends ServiceImpl<FileInfoMapper, FileInfo> {
@Resource
private FolderInfoMapper folderInfoMapper;
/**
* 查询了目录下的文件列表集合
*
@ -31,4 +39,26 @@ public class FileService extends ServiceImpl<FileInfoMapper, FileInfo> {
q.last("limit 1");
return this.getOne(q);
}
public FileInfo fastUpload(int uid, FastUploadFile file) {
long folderId = 0;
if (!"/".equals(file.getFolder())) { // 不是根目录
final FolderInfo folder = folderInfoMapper.getByPath(uid, file.getFolder());
if (null == folder) throw BizException.create("保存目录不存在");
folderId = folder.getId(); // 设置上传目录编号
}
FileInfo info = this.getByMd5(file.getHash());//查询是否存在要上传的文件
if (info == null) return null; // 没有存在 只能走普通上传
//
FileInfo newFile = new FileInfo(); // 要保存的数据
newFile.setPath(info.getPath());
newFile.setHash(info.getHash());
newFile.setType(file.getType());
newFile.setFolderId(folderId);
newFile.setUid(uid);
newFile.setName(file.getName());
newFile.setSize(info.getSize());
return this.save(newFile) ? newFile : null;
}
}

View File

@ -26,10 +26,7 @@ public class FolderService extends ServiceImpl<FolderInfoMapper, FolderInfo> {
* @return
*/
public FolderInfo getByPath(int uid, String path) {
QueryWrapper q = new QueryWrapper();
q.eq("uid", uid);
q.eq("path", path);
return this.getOne(q);
return this.getBaseMapper().getByPath(uid, path);
}
/**
@ -85,7 +82,7 @@ public class FolderService extends ServiceImpl<FolderInfoMapper, FolderInfo> {
fNew.setUid(uid);
fNew.setName(name);
fNew.setParentId(parentId);
fNew.setPath((parent.equals("/")?"":parent) + "/" + name);
fNew.setPath((parent.equals("/") ? "" : parent) + "/" + name);
return this.save(fNew) ? fNew : null;
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="xyz.longicorn.driver.dao.FolderInfoMapper">
<select id="getByPath" resultType="FolderInfo">
select * from folder_info where uid=#{uid} and path=#{path} limit 1
</select>
</mapper>

1
web/package-lock.json generated
View File

@ -22,6 +22,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"@vue/reactivity": "^3.2.33",
"vite": "^2.9.7"
}
},

View File

@ -22,6 +22,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"@vue/reactivity": "^3.2.33",
"vite": "^2.9.7"
}
}

View File

@ -5,6 +5,7 @@ import {dayjs, ElMessage, ElMessageBox} from 'element-plus'
import api from "./service/api";
import qs from "qs";
import FileUploader from "./components/file-uploader/Index.vue";
import FileBlockItem from "./components/FileBlockItem.vue";
export default {
setup(){
@ -80,7 +81,7 @@ export default {
return pathList;
}
},
components: {FileUploader, FileIcon, ArrowDown, Grid, FolderAdd},
components: {FileBlockItem, FileUploader, FileIcon, ArrowDown, Grid, FolderAdd},
mounted() {
// window.addEventListener('popstate',()=>console.log(location.href))
window.addEventListener('hashchange', this.handleHashChange) //
@ -128,18 +129,6 @@ export default {
ext = name.substr(extIndex);
return name.substr(0, (15 - ext.length)) + "**" + ext;
},
showFile(file, e) {
console.log(e)
if (file.type != 'folder') {
console.log(this.$refs.fileIcon)
this.$refs.fileIcon?.showPreview(file);
return;
}
// this.loadFileByPath(file.path)
// console.log(file)
// window.history.pushState({},null,'/show?path=' + file.name)
location.hash = '?path=' + file.path
},
fileMenuClick(e) {
window.alert(JSON.stringify(e));
},
@ -335,18 +324,7 @@ export default {
<el-row :gutter="20">
<el-col :xs="12" :sm="6" :md="4" :lg="3" :xl="2" v-for="(file, index) in fileData"
:key="index">
<div class="file-block-item" @click="showFile(file,$event)"
@contextmenu.prevent.stop="$refs.fileMenu.showMenu($event,file)">
<div class="file-image">
<FileIcon :file="file" style="width:90%"/>
</div>
<div class="file-name">{{ formatName(file.name) }}</div>
<div class="file-info">
{{
file.type == 'folder' ? formatDate(file.createTime) : formatSize(file.size)
}}
</div>
</div>
<file-block-item :file="file" />
</el-col>
</el-row>
</div>

View File

@ -0,0 +1,74 @@
<template>
<div class="file-block-item" @click="showFile(file,$event)"
@contextmenu.prevent.stop="$parent.$refs.fileMenu.showMenu($event,file)">
<div class="file-image">
<FileIcon :file="file" ref="fileItemIcon" style="width:90%"/>
</div>
<div class="file-name">{{ formatName(file.name) }}</div>
<div class="file-info">
{{
file.type == 'folder' ? formatDate(file.createTime) : formatSize(file.size)
}}
</div>
</div>
</template>
<script>
import {ref} from "@vue/reactivity";
import {dayjs} from "element-plus";
import FileIcon from "./FileIcon.vue";
export default {
name: "FileBlockItem",
components: {FileIcon},
props: {
file: {
type: Object, request: true
}
},
/**
*
* @param {file:FileItem} props
*/
setup(props) {
/**
*
* @type {FileIconInstance}
*/
const fileItemIcon = ref(null)
const showFile = (file) => {
if (file.type != 'folder') {
console.log(fileItemIcon)
// fileItemIcon.showPreview();
return;
}
location.hash = '?path=' + file.path
}
return {
file: props.file, showFile, fileItemIcon
}
},
methods: {
formatDate(time, format = 'MM-DD') {
return dayjs(time).format(format);
},
formatSize(a, b = 2) {
if (0 == a) return "0 B";
let c = 1024, d = b || 2, e = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
f = Math.floor(Math.log(a) / Math.log(c));
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
},
//
formatName(name) {
if (!name || name.length < 15) return name;
const extIndex = name.lastIndexOf('.'),
ext = name.substr(extIndex);
return name.substr(0, (15 - ext.length)) + "**" + ext;
},
}
}
</script>
<style scoped>
</style>

View File

@ -75,8 +75,9 @@ import FileUpload from "./FileUpload.vue";
import {Upload, CircleCloseFilled, CloseBold, ArrowDownBold} from '@element-plus/icons-vue'
import Hash from 'browser-md5-file';
import api from "../../service/api";
// hash
const hash = new Hash();
const FileStatus = {
Processing: 0,
Ready: 1,
@ -87,11 +88,11 @@ const FileStatus = {
export default {
name: "Index",
emits:{
emits: {
/**
* 文件上传后的触发此事件
*/
uploadSuccess:null
uploadSuccess: null
},
props: {
/**
@ -129,19 +130,63 @@ export default {
const index = this.fileListData.indexOf(file);//
this.fileListData.splice(index, 1)
},
/**
*
* @param {FileList} files
*/
add(files) {
this.showUploadList = true;
Array.from(files).forEach(file => {
this.fileListData.push({
id: this.fileId++,
file,
name: file.name,
status: FileStatus.Ready,
folder:this.currentFolder, //
progress: 0,
})
const size = file.size / 1024 // KB
if (size < 1024) { // 1024KB
hash.md5(file, (err, md5) => {
this.fastUpload(file, md5);
}, p => console.log(p))
} else {
this.pushUploadFile(file, this.currentFolder)
}
})
this.startUpload();//
},
//
pushUploadFile(file, folder, status = FileStatus.Ready, progress = 0) {
const f = {
id: this.fileId++,
file,
name: file.name,
status,
folder, //
progress,
};
this.fileListData.push(f);
this.uploadFile(f);//
},
/**
*
* @param {File} file
* @param hash
*/
fastUpload(file, hash) {
const folder = this.currentFolder;
api.file.fastUpload({
name: file.name,
size: file.size,
type: file.type,
hash,
folder
}).then(ret => {
if (ret) {
this.$emit('upload-success', {
result: ret,
file: file
})
//
this.pushUploadFile(file, folder, FileStatus.Success, 100)
} else {
this.pushUploadFile(file, folder);
}
}).catch(e => {
this.pushUploadFile(file, folder);
});
},
startUpload() {
this.fileListData.forEach(f => {
@ -152,17 +197,19 @@ export default {
})
},
uploadFile(f) {
f.status = FileStatus.Uploading;
api.upload(f.folder, f.file, (p) => {
f.progress = p.progress
}).then(ret => {
console.log(ret)
f.status = FileStatus.Success; //
this.$emit('upload-success', {
result: ret.data,
file: f
})
}).catch(e => console.log(e));
if (f.status == FileStatus.Ready) {
f.status = FileStatus.Uploading;
api.file.upload(f.folder, f.file, (p) => {
f.progress = p.progress
}).then(ret => {
console.log(ret)
f.status = FileStatus.Success; //
this.$emit('upload-success', {
result: ret.data,
file: f
})
}).catch(e => console.log(e));
}
}
}

View File

@ -74,40 +74,50 @@ export default {
return request(`/api/folder/create`, 'POST', {parent, name});
}
},
upload(parent, file, onProcess = null) {
// 上传文件到某个目录
const postData = new FormData(); // 将数据封装成form表单
postData.append("parent", parent); // 父目录
postData.append("file", file);// 文件
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open('POST', API_PATH + '/api/upload');
request.upload.addEventListener('progress', function (e) {
// upload progress as percentage
let progress = (e.loaded / e.total) * 100;
onProcess({
uploaded: e.loaded,
total: e.total,
progress
})
});
request.addEventListener('load', function (e) {
// HTTP status message (200, 404 etc)
let ret = {message: null}
try {
ret = JSON.parse(request.response)
if (request.status == 200) {
resolve(JSON.parse(request.response));
} else {
reject(Error(ret.message || '上传文件出错!'));
file: {
/**
* 快速上传
* @param params
* @returns {Promise<unknown>}
*/
fastUpload(params) {
return request('/api/fast-upload', 'POST', params);
},
upload(parent, file, onProcess = null) {
// 上传文件到某个目录
const postData = new FormData(); // 将数据封装成form表单
postData.append("parent", parent); // 父目录
postData.append("file", file);// 文件
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest();
request.open('POST', API_PATH + '/api/upload');
request.upload.addEventListener('progress', function (e) {
// upload progress as percentage
let progress = (e.loaded / e.total) * 100;
onProcess({
uploaded: e.loaded,
total: e.total,
progress
})
});
request.addEventListener('load', function (e) {
// HTTP status message (200, 404 etc)
let ret = {message: null}
try {
ret = JSON.parse(request.response)
if (request.status == 200) {
resolve(JSON.parse(request.response));
} else {
reject(Error(ret.message || '上传文件出错!'));
}
} catch (e) {
console.log(e)
reject(Error('上传文件异常'))
}
} catch (e) {
console.log(e)
reject(Error('上传文件异常'))
}
});
request.send(postData);
})
// return request('/api/upload', 'FILE', postData, onProcess)
});
request.send(postData);
})
// return request('/api/upload', 'FILE', postData, onProcess)
}
}
}

View File

@ -11,4 +11,12 @@ declare type FileItem = {
size: number,
type: 'folder' | string,
updateTime: string
}
}
declare type FileIconInstance = {
file: FileItem,
ext: String,
/**
*
*/
showPreview: Function
}