封面由 AI 生成,仅作主题示意。
封面由 AI 生成,仅作主题示意。
图片文件叫“最终版.png”,后来同名文件又被覆盖了,上传记录却仍然写着旧名称。只按文件名核对资产,就可能分不清上次实际用的是哪份内容。这里实现一个本地文件清单函数:为文件读取字节数与SHA-256,并把调用方提供的商品信息一起记录。
它不判断画面是否美观、颜色是否准确,也不签发真实性凭证。文件指纹用于排查文件内容变化;同一个商品重新压缩、修改元数据或换编码,都可能改变它。判断两张图是否“视觉相同”是另一项任务,不属于本文实现。
清单里把计算字段与业务字段分开
Picset 是可产生商品图片素材的一条工具路径,页面提供详情图制作。[1] 素材也可以来自摄影、设计软件或供应商。本函数只处理已保存到本地的文件,不调用产品接口,不假设Picset支持某种API,也不把工具名称当作图片已经获准上架的证明。
文件路径、字节数与哈希由程序获得;商品编码、包装版本、用途和审核状态由业务流程提供。代码可以记录一行“待确认”,却不能通过文件指纹推导“已审核”。把这两个来源分开,后续才知道该去修文件还是修业务登记。
AI 技术概念图,不是实际哈希值、程序输出或安全认证。
下面限定文件在指定素材目录内,允许子目录;通过realpath再检查一次符号链接解析后的路径,拒绝指向目录外的文件。读入前限制体积,示例默认20 MiB,这是项目参数,不是电商平台规范,也不是完整的安全隔离。
实现一个不修改原图的函数
保存为 asset-manifest.cjs。示例用Node.js标准库,SHA-256接口依据官方crypto文档。[2]
const fs = require('node:fs/promises');
const path = require('node:path');
const { createHash } = require('node:crypto');
function within(root, file) {
const rel = path.relative(root, file);
return rel !== '' && !path.isAbsolute(rel) &&
rel !== '..' && !rel.startsWith('..' + path.sep);
}
async function assetRecord(rootDir, relative, metadata = {},
maxBytes = 20 * 1024 * 1024) {
if (typeof relative !== 'string' || !relative ||
path.isAbsolute(relative)) throw new TypeError('需相对路径');
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
throw new TypeError('体积上限无效');
const root = await fs.realpath(rootDir);
const candidate = path.resolve(root, relative);
if (!within(root, candidate)) throw new Error('路径超出素材目录');
const actual = await fs.realpath(candidate);
if (!within(root, actual)) throw new Error('链接指向目录外');
const info = await fs.stat(actual);
if (!info.isFile()) throw new Error('目标不是文件');
if (info.size > maxBytes) throw new Error('文件过大');
const bytes = await fs.readFile(actual);
if (bytes.length > maxBytes) throw new Error('读取后文件过大');
return {
metadata,
file: relative.split(path.sep).join('/'),
bytes: bytes.length,
sha256: createHash('sha256').update(bytes).digest('hex')
};
}
module.exports = { assetRecord };
调用时传入素材根目录、文件相对路径和业务字段,例如 assetRecord('./assets', '01.png', { sku: 'DEMO-01', status: '待确认' })。其中编码是示例,不对应真实商品。函数返回Promise;在调用脚本中使用await,或者then与catch处理结果。需要批量清单时,先逐文件执行并记录失败项,别因为一个文件不存在就默默跳过它。
返回对象里保留的是调用时读到的文件字节。以后再对同一文件生成记录,可以比较哈希,并检查业务版本是否也变化。文件改名但字节没变,哈希可以保持相同;两份业务记录的商品编码是否允许共用图片,仍应由业务规则决定。不能因为哈希相同,就自动合并不同商品。
测试时不要只检查正常文件
本批交付用本机Node实际验证了正常文件、重复读取、同字节改名、字节变化、中文与子目录路径、目录目标、体积上限和目录逃逸等分支,具体结果随包保存。测试不是对任意环境的安全保证,也不证明所有图片可解码。
函数没有检查扩展名、文件签名或图片宽高。即使把文本文件改名成PNG,它也能产生字节指纹,不能因此将其认定为图片。若需要图像类型预检或解码,应接另一道处理,不把几种不同检查合成一个模糊的“素材合格”状态。
还有并发修改边界。检查路径、读取文件之间,文件系统状态可能变化;这个示例适合受控素材目录的日常记录,不用于处理不可信用户能够同时改动的文件树。大目录也不宜无限并发读取,批量版本需要另设并发与错误记录策略。
将清单放进图片交付流程时,先确定它帮助回答哪个问题:某份发布记录对应的文件是否改变,还是商品资料已经换版?前者可以由指纹协助定位,后者需要商品版本字段。把指纹当作辅助记录,才不会让技术字段承担它无法提供的内容判断。