专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【第3398期】Vue项目基于源码实现可视化 ... ·  2 天前  
前端早读课  ·  【早阅】滚动页面布局形式的探索 ·  3 天前  
前端早读课  ·  【第3396期】Monaco Editor ... ·  4 天前  
前端之巅  ·  对话Motiff妙多赵薇:大模型在UI设计工 ... ·  1 周前  
前端早读课  ·  【早阅】可能不知道的JavaScript的6件事 ·  1 周前  
51好读  ›  专栏  ›  前端早读课

【第3399期】如何为上传文件取一个唯一的文件名

前端早读课  · 公众号  · 前端  · 2024-10-24 08:00

正文

前言

介绍了如何为上传到 CDN 平台的文件生成一个唯一的文件名,并探讨了几种不同的命名方法,最终提出了一种基于时间戳和随机数的优化方案,以确保文件名的唯一性和简短性。今日前端早读课文章由 @陈杰分享,公号:Goodme 前端团队授权分享。

正文从这开始~~

背景

古茗内部有一个 CDN 文件上传平台,用户在平台上传文件时,会将文件上传至阿里云 OSS 对象存储,并将 OSS 链接转换成 CDN 链接返回给用户,即可通过 CDN 链接访问到文件资源。我们对 CDN 文件的缓存策略是持久化强缓存 (Cache-Control: public, max-age=31536000),这就要求所有上传文件的文件名都是唯一的,否则就有文件被覆盖的风险。有哪些方式可以保证文件名全局唯一?

唯一命名方式

方式一:使用时间戳 + 随机数

这是我们最容易想到的一种方式:

 const name = Date.now() + Math.random().toString().slice(2, 6);
// '17267354922380490'

使用时间戳,加上 4 位随机数,已经可以 99.99999% 保证不会存在文件名重复。可以稍微优化一下:

 const name = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
// 'm191x7bii63s'

将时间戳和随机数分别转换成 36 进制,以减少字符串长度。通过上面一步优化可以将字符长度从 17 位减少至 12 位。

使用时间戳 + 随机数作为文件名的优势是简单粗暴,基本上可以满足述求;但是有极小概率存在文件名冲突的可能。

方式二:使用文件 MD5 值

生成文件的 MD5 Hash 摘要值,在 node 中代码示例如下:

 const crypto = require('crypto');
const name = crypto.createHash('md5').update([file]).digest('hex');
// 'f668bd04d1a6cfc29378e24829cddba9'

文件的 MD5 Hash 值可以当成文件指纹,每个文件都会生成唯一的 hash 值(有极小的概率会 hash 碰撞,可以忽略)。使用 MD5 Hash 值作为文件名还可以避免相同文件重复上传;但是缺点是文件名较长。

方式三:使用 UUID

UUID (通用唯一识别码) 是用于计算机体系中以识别信息的一个标识符,重复的概率接近零,可以忽略不计。
生成的 UUID 大概长这样:279e573f-c787-4a84-bafb-dfdc98f445cc。

使用 UUID 作为文件名的缺点也是文件名较长。

【第3248期】提升用户体验的UUID设计策略

最终方案

从上述的几种命名方式可以看出,每种方式都有各种的优缺点,直接作为 OSS 的文件命名都不是很满意(期望 CDN 链接尽可能简短)。所以我们通过优化时间戳 + 随机数方式来作为最终方案版本。

本质上还是基于时间戳、随机数 2 部分来组成文件名,但是有以下几点优化:

  • 由于 CDN 链接区分大小写,可以充分利用 数字 + 大写字母 + 小写字母(一共 62 个字符),也就是可以转成 62 进制,来进一步缩短字符长度

  • 时间戳数字的定义是,当前时间减去 1970-01-01 的毫秒数。显然在 2024 年的今天,这个数字是非常大的。对此,可以使用 当前时间减去 2024-01-01 的毫秒数 来优化,这会大幅减少时间戳数字大小(2024-01-01 这个时间点是固定的,而且必须是功能上线前的一个时间点,确保不会减出负数)

示例代码如下:

 /**
* 10 进制整数转 62 进制
*/

function integerToBase62(value) {
const base62Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const base62 = base62Chars.length;
value = parseInt(value);
if (isNaN(value) || !value) {
return String(value);
}

let prefix = '';
if (value < 0) {
value = -value;
prefix = '-';
}

let result = '';
while (value > 0) {
const remainder = value % base62;
result = base62Chars[remainder] + result;
value = Math.floor(value / base62);
}

return prefix + result || '0';
}

const part1 = integerToBase62(Date.now() - new Date('2024-01-01').getTime()); // 'OkLdmK'
const part2 = integerToBase62(Math.random() * 1000000).slice(-4); // '3hLT'
const name = part1 + part2; // 'OkLdmK3hLT'

最终文件名字符长度减少到 10 位。但是始终感觉给 4 位随机数太浪费了,于是想着能否在保证唯一性的同时,还能减少随机数的位数。那就只能看看时间戳部分还能不能压榨一下。

只要能保证同一毫秒内只生成一个文件的文件名,就可以保证这个文件名是唯一的,这样的话,随机数部分都可以不要了,所以可以做如下优化:

 // 伪代码
async function getFileName() {
// 等待锁释放,并发调用时保证至少等待 1ms
await waitLockRelease();

return integerToBase62(Date.now() - new Date('2024-01-01').getTime());
}

const name = await getFileName();
// 'OkLdmK'

由于 node 服务线上是多实例部署,所以 waitLockRelease 方法是基于 Redis 来实现多进程间加锁,保证多进程间创建的文件名也是唯一的。与此同时,还额外加上了一位随机数,来做冗余设计。最终将文件名字符长度减少至 7 位,且可以 100% 保证唯一性!

总结

看似非常简单的一个问题,想要处理的比较严谨和完美,其实也不太容易,甚至引入了 62 进制编码及加锁逻辑的处理。希望本文的分享能给大家带来收获!

关于本文
作者:@陈杰
原文:https://mp.weixin.qq.com/s/YJ1iQtUukXvh16vReSTUoQ

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。