# base-easy-uploader **Repository Path**: ldp9261/base-easy-uploader ## Basic Information - **Project Name**: base-easy-uploader - **Description**: 更简单的文件上传实现 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 6 - **Created**: 2022-12-04 - **Last Updated**: 2022-12-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 轻量级资源管理器 ## 介绍 本项目旨在实现一个轻量级的资源管理器,即可以作为一个简易的Oss云存储服务器,也可以作为多人使用的资源管理器,核心设计思想如下图所示: ![](设计图.png) (ps: 1.0版本应用方向旨在实现一个简易的大文件分片上传工具,因此并没有在设计之初考虑添加多租户和桶机制,因此本项目正处于重写阶段,全新版本将以2.0发布) **** ## 核心功能介绍 1. 支持大文件分片上传 2. 支持大文件断点续传 3. 支持文件和分片的秒传 4. 支持上传目录,并保存目录结构不变 5. 支持下载目录,目录将以压缩包形式下载 **** ## 资源隔离 普通用户只能访问到自身家目录下的资源和共享空间下的资源。 (ps: 用户对共享空间下的资源,只具备读权限) 如果尝试访问其他资源,将受到非法访问警告。 **** ## 资源共享 A用户上传的文件,如果B用户已经上传到了服务器上,那么A用户上传的文件就无需重复存放在服务器上了。 A直接在自身目录下创建一个硬链接指向B用户已经上传完成的文件即可。 (ps: 不能创建软链接,因为软链接对应的文件一旦被删除,那么该软链接也会失效,而如果我们要删除一个被硬链接指向的文件,如果移除对应inode时,发现硬链接数量不为0,那么就不会清除文件对应的数据块) **** ## 写时复制 A用户上传的文件,B用户已经上传过了,此时只是简单帮A用户建立一个硬链接指向B已经上传的文件。 但是如果此时A或者B用户要修改文件,那么他们不能对原文件进行修改,而需要将原文件COPY一份然后进行修改,然后删除旧文件的inode, 将修改后的文件作为新文件保存。 (ps: 这里针对的是硬链接数量超过1个的文件,此时对于文件数据块来说,相当于有多个inode指向同一个文件数据块,那么删除其中一个inode,实际的文件数据也不会被删除。) **** ## 重删压缩 重删即对重复数据块只保留一份副本。 相关资源参考 https://blog.51cto.com/u_13559412/2057144 https://blog.51cto.com/u_13559412/category1.html 此部分正在研究,暂时在此提上一嘴。 **** ## 认证中心 本项目认证中心和主体功能分开实现的,方便认证中心模块的复用,方便单独对认证中心模块进行升级迭代,而不影响其他应用。 认证中心采用RABC权限模型进行设计: ![](认证中心设计架构图.png) (ps: 简陋的1.0版本,日后有时间会不断更新) **** ### 认证中心与模块间的通信 其他模块如何依靠认证中心完成token认证,用户权限获取,以及获取访问某个资源需要的权限信息呢? ![](authenticateCenter/architecture.png) 上图给出的是认证中心如何在微服务场景下进行工作,模块与认证中心之间的通信全部依靠权限插件完成,权限插件是可拔插的,因此如果一个模块想要拥有权限认证功能,只需要插入权限插件即可。 下面在给出认证中心是如何在本项目中进行工作的流程图: ![](认证中心与资源管理模块通信过程.png) 说明: 1. 当用户向资源管理器发出某个请求后,权限插件中的token过滤器首先拦截该请求,然后向认证中心发送验证token请求 2. 如果认证中心校验该token没有问题,那么就返回UserDetails并在响应头中附上更新过期时间后的token 3. 如果认证中心校验token发现问题,那么就返回null 4. token校验无误后,我们可以利用权限插件提供的前置和后置成功校验回调接口,在前置回调中我们将当前用户信息放入线程上下文中保存 5. 在后置回调接口中,我们清除放入线程上下文中的用户信息 6. token校验没有问题,接下来权限插件会向认证中心发送请求,获取访问当前资源需要的角色集合,如果得到的结果为null,说明访问资源不需要具备任何权限,可以匿名访问 7. 如果返回的角色集合不为null,那么需要从SecurityContext的上下文中取出认证主体,即UserDetails,判断当前用户是否具备相关角色,如果是的话,放行,否则响应403无权限访问 **** ## 大文件上传前置知识科普 ### 秒传 #### 1、什么是秒传 通俗的说,你把要上传的东西上传,服务器会先做 MD5 校验,如果服务器上有同样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让 MD5 改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5 就变了,就不会秒传了. #### 2、实现秒传常见做法 a、利用 redis 的 set 方法存放文件上传状态,其中 key 为文件上传的 md5,value 为是否上传完成的标志位; b、当标志位为 true 表示上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为 false,则说明还没上传完成,此时需要再调用 set 方法,保存块号文件记录的路径,其中 key 为上传文件的 md5 + 一个固定前缀,value 为块号文件的记录路径 ### 分片上传 #### 1、什么是分片上传 分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为 Part)来进行上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。 #### 2、分片上传的场景 1.大文件上传 2.网络环境环境不好,存在需要重传风险的场景 ### 断点续传 #### 1、什么是断点续传 断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。 PS:本文的断点续传主要是针对断点上传场景。 #### 2、应用场景 断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。 #### 3、实现断点续传的核心逻辑 在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。 为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。 **** ### 实现思路 - [ ] 前端对文件进行MD5加密,并且将文件按一定的规则分片 - [ ] 前端发送get请求校验分片数据在服务端是否完整,如果完整则进行秒传,如果不完整或者无数据,则进行分片上传。 - [ ] 后台校验MD5值,根据上传的序号和分片大小计算相应的开始位置并写入该分片数据到文件中。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/fa2acdb495b548c5909cc636603cc58f.png) **** ### 坑 这里只讲用java后端实现文件上传时会遇到的一些坑和前置知识: - [ ] RandomAccessFile文件随机读写流,这个类比较简单,大家自行了解一下即可 - [ ] MappedByteBuffer文件内存映射,底层通过mmap实现, 通过将文件直接映射到用户空间,可以减少系统调用和内存拷贝次数,从而大大提高大文件传输性能,具体使用和原理大家参考此篇文章: [神奇的MappedByteBuffer](https://blog.csdn.net/m0_53157173/article/details/127584591?spm=1001.2014.3001.5501) - [ ] 加密算法: [java——加密、解密算法](https://qkongtao.cn/?p=580#h3-7) 因为我是直接使用java来mock客户端的,因此就选用了RestTemplate来作为发送请求的工具,但是使用RestTemplate来发送文件时,存在一些小坑,大家需要注意,具体如何使用RestTemplate发送文件,大家可以参考此篇文章: [使用RestTemplate上传文件](https://juejin.cn/post/7036365306574405640) **** ### 3.测试客户端 因为本项目并没有提供任何前端界面,只提供了相关接口,通过knife4j提供的API界面进行展示。 默认端口为5200,大家启动项目后可以访问: http://localhost:5200/doc.html 查看API界面。 默认提供的客户端在test包下,通过simpleClientUploader提供的uploadFile方法即可完成文件上传: ```java //文件或者目录的路径--不存在抛出异常 //存储桶名称(前提是存在对应的存储桶)--可以设置为null,表示启用默认存储桶 //dirPath: 将当前文件上传到存储桶下的哪一个目录下,可以传null,那么上传的文件或者目录将直接存放到存储桶下 simpleClientUploader.uploadFile(null,null,null) ``` **** # 数据库结构介绍 - 用户表结构 ```sql DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名', `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码', `eamil` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '邮箱', `head_img` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '头像', `score` int(50) NOT NULL DEFAULT 0 COMMENT '积分', `home_dir_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '家目录名字', `bucket_num` int(50) NOT NULL DEFAULT 0 COMMENT '创建桶数量', `file_upload_num` int(50) NOT NULL DEFAULT 0 COMMENT '上传文件数量', `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '用户注册时间', `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '用户信息最后一次更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; ``` - 存储桶表结构 ```sql DROP TABLE IF EXISTS `bucket`; CREATE TABLE `bucket` ( `id` int(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '存储桶名称', `bucket_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '存储桶类型: 0-只读,1-读写', `access_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '访问类型: 0-私有,1-公开', `create_by` int(20) NOT NULL COMMENT '创建者id', `update_by` int(20) NOT NULL COMMENT '更新者id', `system` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否是系统资源: 0-不是,1-是 ', `file_num` int(11) NOT NULL DEFAULT 0 COMMENT '存储桶下的文件数量', `path` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '存储桶相对于baseDir的路径,如: system/private', `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; ``` - 文件表结构 ```sql DROP TABLE IF EXISTS `file_storage`; CREATE TABLE `file_storage` ( `id` int(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `real_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件原本的名称', `renamed_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '添加了防止重复后缀之后的文件名', `suffix` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '后缀', `file_save_path` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '本地文件路径', `file_visit_path` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件访问的相对路径: bucketName+dirPath+fileName', `type` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '类型', `size` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '大小', `identifier` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'md5校验码', `bucket_id` int(20) NOT NULL COMMENT '当前文件所属存储桶ID', `create_by` int(20) NOT NULL COMMENT '创建者', `update_by` int(20) NOT NULL COMMENT '更新者', `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '文件存储' ROW_FORMAT = Compact; ``` - 分片表结构 ```sql DROP TABLE IF EXISTS `file_chunk`; CREATE TABLE `file_chunk` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `file_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件名', `directory_path` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '目录路径', `bucket_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '桶名', `chunk_number` int(11) NOT NULL COMMENT '当前分片标号,从1开始', `chunk_size` float NOT NULL COMMENT '当前分片大小', `data_size` float NOT NULL COMMENT '当前分片内实际装载数据大小', `total_size` float(20, 0) NOT NULL COMMENT '分片关联文件的总大小', `total_chunk` int(11) NOT NULL COMMENT '总分片数', `file_identifier` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '当前文件对应的md5校验码', `chunk_identifier` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '当前分片对应的md5校验码', `upload_by` int(10) NOT NULL COMMENT '上传分片的用户id', `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; ``` 注意点: - 分片表可以使用redis替代,因为当一个文件的所有分片被完整上传后,相关分片信息就可以清除了,本项目初衷是作为一个教学项目,所以采用了更为直观的分片表来告诉各位如何处理文件上传具体细节 - 如果数据量非常大的情况下,可以考虑采用分表措施,这里主要瓶颈可能会出现在文件表,因为所有用户上传的文件信息都保存在文件表中,具体文件表分表措施如下: - 可以考虑起初将文件表命名file_storage-0,该表只存放用户id在0-1万范围内用户上传的文件信息 - file_storage-1存放用户id在1-2万之间用户上传的文件信息 - 可以考虑采用中间件完成路由和自动分表的相关功能 **** # 注意 本项目1.0版本处于废弃状态,2.0全新版本正在开发中 **** # 感言 如果各位小伙伴对这个小项目有不理解的地方,或者有更好的设计思路和想法,或者想一起参与开发,可以私聊我: ![img.png](img.png) ![img_1.png](img_1.png)