vivo 企业云盘服务端实现简介

作者:来自 vivo 互联网存储团队- Cheng Zhi

本文将介绍企业云盘的基本功能以及服务端实现。

一、背景

vivo 企业云盘是一个企业级文件数据管理服务,解决办公数据的存储、共享、审计等文件管理需求;同时便于团队成员快速共享、管理文件,帮助集中管理企业数字资产,提升办公效率,实现内部数据资源的共享以及与外部客户之间的文件安全交换。

二、功能介绍

目前 vivo 企业云盘有 3 个空间:个人空间,团队空间和备份空间。

2.1 个人空间

个人空间用于存储用户个人的文件数据,其他用户不可见;容量默认为 100GB。个人空间支持文件的分享、下载、移动、重命名、星标、机房下载和删除操作,如下图所示:

2.2 团队空间

团队空间用于多人协作,团队中可容纳多名成员,每个成员都可以向团队空间中上传文件并与其他人共享这些文件,也可以下载其他人上传到该团队空间的文件;团队空间没有容量限制。

用户可以在如下位置创建团队空间:

团队空间的创建者默认为该空间的管理员,管理员可以在左边菜单栏中的团队空间下看到"团队设置"和"成员管理",在"团队设置"页可以修改该团队空间的名称和团队描述信息:

在"成员管理"页可以添加成员并修改已有成员的权限:

2.3 合作伙伴

团队空间中除了内部员工还可以加入外部合作伙伴,管理员可在如下页面申请合作伙伴账号:

点击"新增"后在弹出的"申请外部用户账号"页填写合作伙伴相关信息即可提交直接上级领导审批,审批通过后会在该团队空间中生成一个合作伙伴账号,账号及初始密码会以邮件形式发送到合作伙伴邮箱,合作伙伴登录后即可上传文件或下载分享给他的文件。

出于数据安全的考虑,合作伙伴无法看到团队空间中内部员工上传的文件,只能看到自己上传的文件以及分享给他的文件。

管理员可以在"成员管理"页禁用合作伙伴账号:

2.4 备份空间

备份空间用于备份用户本地电脑上的文件。目前企业云盘网页端只能查看已有的备份策略,新建备份策略需要在企业云盘客户端进行;用户可以在企业云盘网页端右上角的"客户端下载"下载企业云盘客户端:

在客户端的"备份同步"页点击"新增备份",然后在弹出的对话框中选择想要备份的本地文件夹即可创建备份记录:

企业云盘客户端将按用户设置的频率将指定文件夹下的文件上传到对象存储以实现文件备份;对于实时备份,企业云盘客户端会每 3 分钟扫描一次本地文件夹,并与远程的文件进行对比,将新增的文件上传到对象存储。

三、功能实现

企业云盘的存储分为元数据和对象存储两部分,元数据存储使用的是 MySQL,保存的是用户,群组以及文件等实体的元数据,文件的实际数据是以对象的形式保存在对象存储中。企业云盘架构如下:

下面介绍一下各个功能是如何实现的:

3.1 用户认证鉴权

企业云盘在用户的身份验证中使用了非对称加密,前端持有一个公钥,后端持有一个私钥,用户登录时,前端首先获取浏览器指纹 webFinger,同时生成一个随机数种子 seed,然后用公钥计算出一个特征字符串 RSA(webFinger+seed),然后将此字符串放入请求 header 中的 finger 字段,传递给服务端;另外企业云盘接入了 uuc 单点登录系统,uuc 登录成功后会在请求的 cookie 字段中放置 uuc-token 和 uuc-uuid,这两个值也会传给后端。

服务端收到登录请求后,先使用 cookie 中的 uuc-token 以及 uuc-uuid 调用 uuc 接口查询得到用户 uid, 然后尝试从 user 表中查询用户信息,如果查询不到那么说明用户是第一次登录企业云盘,那么服务端会从 uuc 获取用户信息并存储在 user 表中;然后服务端利用私钥解密登录请求中的特征字符串,得到 webFinger,再根据 webFinger + 当前时间 + uid 进行 AES 加密得到一个字符串 clouddisk-token,并将 clouddisk-token 放置在 cookie 中,返回给客户端。在发送后续请求时,客户端需要将 clouddisk-token 保持在 cookie 中。

在后续请求中,客户端以同样的方式生成 finger,并且在请求中携带 clouddisk-token;服务端接收到请求后,将 clouddisk-token 进行AES解密,获取 uid + 时间 + webFinger,同时服务端根据自身持有的私钥,对 header 中的 finger 解密,获取此 finger 对应的 webFinger,与解密 token 得到的 webFinger 对比,如果相等,则验证通过。以上过程如下图所示:

团队空间的数据保存在 groups 表中,该表会记录团队名称、创建人等信息;用户与团队空间的归属关系保存在 group_usrs 表中,该表会记录每个团队空间有哪些用户,以及这些用户在团队空间中的权限。

在个人空间中用户对文件有最高权限,可以任意操作;当用户操作的文件属于某个团队空间时前端会在请求中携带 group_id,服务端会根据 group_id 查询 group_usrs 表,从而获取该用户在该团队空间中的权限,进而判断用户是否有权限执行相应操作。

3.2 文件上传

用户可以通过点击页面的上传按钮然后选择本地文件或拖拽文件/文件夹到企业云盘页面的方式上传文件,除此之外开启备份策略时也会调用上传接口;用户发起上传后,前端会判断文件大小,如果在 10MB 以内则直接上传,否则,对于备份的文件将文件按 10MB 大小分片进行分片上传,其他文件按 5MB 进行分片上传。

所有文件的元数据都保存在 files 表中,该表会记录文件名、文件路径、文件所在空间、文件数据在对象存储中的 key、文件所属用户等信息;所有文件夹的元数据都保存在 folder 表中,该表会记录文件夹的名称、路径、文件夹所在空间、文件夹所属用户等信息。

3.2.1 小文件上传

小文件上传的逻辑如下:

  1. 查数据库获取用户及其所在空间的空间信息;

  2. 空间用量校验;

  3. 查找文件夹,如果文件夹不存在则新建文件夹;

  4. 查找文件,构造新 files 记录:如果文件不存在,则使用原始文件名;如果文件已存在,则在文件名后面拼接序号以区别于原文件;

  5. 上传文件数据到对象存储;

  6. 生成随机字符串作为 file_mark,将第 4 步中的 files 记录插入 files 表。

3.2.2 大文件上传

大文件指采用分片方式上传的文件,文件分片的信息保存在 multi 表中,multi 表会记录分片对应的文件、上传者、分片总数、当前分片编号、upload id 等信息。

大文件分片上传分 3 个步骤:

start 阶段

  1. 查数据库获取用户及所在空间信息,认证鉴权;

  2. 判断文件是否已经存在;

  3. 查找文件夹,如果文件夹不存在则新建文件夹;

  4. 查找文件,构造新 files 记录:如果文件不存在,则使用原始文件名;如果文件已存在,则在文件名后面拼接序号以区别于原文件;

  5. 从对象存储获取用于分片上传的 upload id;

  6. 生成随机字符串作为 file_mark,将第 4 步中的 files 记录插入 files 表;

  7. 将分片记录插入 multi 表;

  8. 将 upload id 返回给客户端,用于后续关联分片;将 file_mark 返回给客户端,用于后续关联文件。

upload 阶段

  1. 查数据库获取用户及所在空间信息,认证鉴权;

  2. 通过 file_mark 获取文件信息;

  3. 通过 upload id 获取文件的分片信息;

  4. 为当前分片生成 multi 表记录;

  5. 将当前分片数据上传到对象存储;

  6. 将第 4 步中的 multi 记录插入 files 表。

complete 阶段

  1. 查数据库获取用户及所在空间信息,认证鉴权;

  2. 通过 file_mark 获取文件信息;

  3. 通过 upload id 获取文件的分片信息;

  4. 通知对象存储进行分片合并操作;

  5. 删除该文件所有分片记录;

  6. 更新目录用量及文件状态。

3.2.3 元数据与对象的对应

以下是使用对象存储 SDK 从对象存储获取对象的示例代码:

go 复制代码
params := &s3.GetObjectInput{
    Bucket: aws.String("BucketName"), // bucket名称
    Key: aws.String("ObjectKey"),     // object key
}
 
resp, err := client.GetObject(params)
if err != nil{
    panic(err)
}
 
//读取返回结果中body的前20个字节
b := make([]byte, 20)
n, err := resp.Body.Read(b)
fmt.Printf("%-20s %-2v %v\n", b[:n], n, err)

可以看到为了从对象存储获取对象只需要提供一个桶名(bucket name)和键名(object key)即可。桶名信息在配置文件中,服务端启动后即会加载到内存中;object key 是通过 "用户工号 + 路径 + 时间戳 + _ + 文件名" 格式拼接成的字符串。

例如:

用户 11*****9 在 2023-12-19 14:15:40 将文件 test.txt 上传到个人空间中 /a/b/c/ 目录下,那么这个文件对应的 object key 就是

11*****9/a/b/c/2023-12-19T14:15:40+08:00_test.txt;

如果这个字符串长度小于 128 字节那么就用这个字符串作为文件的 object key。如果拼接后的字符串长度大于 128 字节,那么服务端会先计算文件路径的 md5 值,记为 md5(path),然后拼接字符串:用户工号 + / + md5(path) + 时间戳 + _ + 文件名,该 object key 生成之后会存入 files 表的 path 字段。

3.2.4 外链上传

企业云盘还支持通过外链将文件从 Linux 机器上传到企业云盘。使用外链上传需要先申请权限,申请通过后企业云盘页面可以看到"机房上传"按钮:

点击该按钮会将命令行复制到剪切板,命令行格式如下:

bash 复制代码
file="在此输入文件名称!";curl -s -X PUT "http://******/clouddisk-prd/******?Expires=******&AWSAccessKeyId=******&Signature=******" -H "x-amz-acl: public-read" -H "x-amz-content-maxlength: 200000000000000000" -H "Content-Type: application/octet-stream" --data-binary "@$file";curl -s -X POST "pan-idc.vivo.xyz/api/file/sync" -H "clouddisk-token: ******"  -H "finger: ******" -H "Content-Type: application/json" -H "path: ******" -H "hashname: ******" -H "filename: $file"

将 "在此输入文件名称!" 部分修改为要上传的文件名然后执行命令行即可上传文件。

该功能实现原理如下:

  1. 查数据库获取用户及所在空间信息,认证鉴权;

  2. 判断文件夹是否存在,不存在则返回错误;

  3. 生成外链。用户点击机房上传时服务端会为文件构造 object key,首先拼接字符串:clouddisk_ + 用户工号 + _ + 当前时间时间戳,然后计算该字符串的 SHA1 哈希值,记为 SHA(ut),然后拼接字符串 "用户工号 + 文件路径 + / + SHA(ut)" 作为将上传的文件的 object key;然后用这个 object key 调用对象存储 sdk 生成预签名 URL 用于上传,这个预签名 URL 就是外链中第一个 curl 命令行请求的 URL。第二个 curl 用于调用企业云盘服务端接口将文件元数据写入 MySQL,包括将 object key 写入 files 表的 path 字段。

可以看到在用户使用外链上传文件时,时间戳起到了关联文件数据与文件元数据的作用,因此用户每次上传都必须重新拷贝链接,而不能复用之前的链接,否则会导致已上传的文件被覆盖。

3.3 文件下载

用户在企业云盘界面选中文件即可下载文件,流程如下:

  1. 查数据库获取用户及所在空间信息,认证鉴权

  2. 判断文件是否存在

  3. 用文件的元数据中的 path 作为 object key 调用对象 SDK 获取文件的预签名 URL

  4. 将预签名 URL 返回给前端,前端根据链接下载文件

另外用户也可以通过机房链接将文件下载到 Linux 的机器上:

或者获取办公网链接,该链接可以在办公网下载文件;这两个链接的获取也是调用的下载文件的接口,只是为了方便在 Linux 系统上下载文件而在前面拼接了 wget。

四、总结

本文简单介绍了 vivo 企业云盘的基本功能,并介绍了这些功能在服务端具体的实现原理,其中重点介绍了认证鉴权和文件的上传下载。希望读者阅读后对 vivo 企业云盘能有更深入的了解,也希望本文能在应用的认证鉴权及文件的上传下载逻辑方面对读者有所启发。

相关推荐
qq_17448285755 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
锅包肉的九珍5 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦5 小时前
Scala的Array(2)
开发语言·后端·scala
2401_882727576 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
心仪悦悦6 小时前
Scala中的集合复习(1)
开发语言·后端·scala
代码小鑫7 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖7 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
激流丶7 小时前
【Kafka 实战】Kafka 如何保证消息的顺序性?
java·后端·kafka
uzong8 小时前
一个 IDEA 老鸟的 DEBUG 私货之多线程调试
java·后端
飞升不如收破烂~8 小时前
Spring boot常用注解和作用
java·spring boot·后端