Harmony os 技术实战|拼豆制图39:导出文件名如何兼顾安全、可读与唯一
高清图导出会先写入context.cacheDir再交给系统相册。文件名若直接拼接用户标题斜杠、反斜杠、冒号或控制字符可能改变路径语义若只使用固定名字多次导出又会互相覆盖。拼豆制图当前采用“固定前缀 清洗后的图案 ID 毫秒时间戳 .png”避免标题进入路径也让绝大多数导出拥有不同名字。本篇从现有fileName()逐字符分析补上空 ID、超长 ID、同毫秒碰撞、扩展名与相册标题等边界形成一套既不越界又能让用户辨认的导出命名策略。一、当前文件名由三段稳定信息组成实现只有三行privatestaticfileName(pattern:Pattern):string{constsafeIdpattern.id.replace(/[^a-zA-Z0-9_-]/g,_);returnpindouzhitu_${safeId}_${Date.now()}.png;}例如pattern.id user-generated-1787020800000 fileName pindouzhitu_user-generated-1787020800000_1787020812345.png允许字符只有英文字母、数字、下划线和连字符路径分隔符、空格、中文与其他符号都会变成下划线。文件名随后拼到应用自己的缓存目录constfilePath${context.cacheDir}/${fileName};二、为什么不直接使用 pattern.title标题适合 UI不一定适合路径。用户标题可能包含猫咪/夏日版 机甲:红色*重制 ../我的作品斜杠可能被视为目录分隔..具有父目录语义部分文件系统或媒体服务还对冒号、星号、问号有额外限制。即使 HarmonyOS 应用沙箱限制了可访问范围也不应把未规范化输入直接拼进路径。业务 ID 通常由应用生成比标题稳定。当前选择 ID 作为内部文件名主体是合理的可读标题可以在相册创建参数中单独提供不必让一个字符串同时承担路径安全和展示文案。三、替换非法字符后还要折叠与裁剪简单替换会产生很长的连续下划线原 ID用户///作品:::001 结果______001可增加标准化步骤functionsafeSegment(raw:string,maxLength:number):string{constreplacedraw.replace(/[^a-zA-Z0-9_-]/g,_);constcollapsedreplaced.replace(/_/g,_);consttrimmedcollapsed.replace(/^_|_$/g,);constlimitedtrimmed.substring(0,maxLength);returnlimited.length0?limited:pattern;}这里有四层语义字符白名单、防止噪声膨胀、去除首尾占位、限制长度最后为空时回退pattern。最大长度可取 48 或 64并给前缀、唯一后缀和扩展名预留空间。四、Date.now 不是绝对唯一 ID毫秒时间戳在普通单按钮导出中足够实用页面的isExporting又阻止同一实例并发。但以下情况仍可能同毫秒生成名字两个 Ability/窗口实例同时导出 后台批量任务并发导出不同图案 单元测试固定或替换系统时间 设备时间发生回拨更强的本地唯一后缀可以组合时间戳和进程内序号classExportNameFactory{privatestaticlastMs:number-1;privatestaticsequence:number0;staticsuffix(now:number):string{if(nowthis.lastMs)this.sequence;else{this.lastMsnow;this.sequence0;}return${now}_${this.sequence};}}跨进程或跨设备严格唯一则应使用系统提供的 UUID/随机能力并核对目标 SDK 接口。当前单进程缓存文件只需确保同一运行期不覆盖时间戳加序号已足够清晰。五、扩展名应该由格式常量统一决定服务打包时指定format:image/png,quality:100相册参数又写fileNameExtension:png文件名同时以.png结尾。三处必须保持一致。若未来支持 JPEG不能只改 packer 格式而继续产生.png文件名。interfaceExportFormat{packerFormat:string;extension:string;}constPNG_FORMAT:ExportFormat{packerFormat:image/png,extension:png};命名、打包和相册创建都读取同一ExportFormat格式变更才不会出现“扩展名是 PNG、内容却是 JPEG”的文件。六、缓存文件要使用 TRUNC 防止旧内容残留打开模式fileIo.openSync(filePath,fileIo.OpenMode.CREATE|fileIo.OpenMode.READ_WRITE|fileIo.OpenMode.TRUNC)即使唯一后缀意外碰撞TRUNC也会在写入前截断旧文件避免新 PNG 比旧 PNG 短时尾部残留旧字节。但它不能解决并发两个任务同时打开同一路径仍会互相覆盖所以唯一命名和导出互斥仍然必要。文件句柄在finally中关闭媒体对话框结束后再尝试删除缓存文件。命名安全、写入模式与清理生命周期是同一条文件链的三个环节。七、相册展示标题可以比缓存文件更友好当前相册标题来自去掉.png的文件名title:fileName.replace(.png,)用户会看到较长的内部 ID 和时间戳。可以把路径名与展示标题分离constdisplayTitlenormalizeDisplayTitle(pattern.title);awaithelper.showAssetsCreationDialog([uri],[{title:displayTitle,fileNameExtension:format.extension,photoType:photoAccessHelper.PhotoType.IMAGE}]);展示标题仍要处理空字符串和过长文本但无需限制为 ASCII因为它不参与缓存路径拼接。建议回退为“拼豆编号图”并把具体编号或日期作为尾部可读信息。不要用replace(.png,)处理任意扩展名可由命名函数同时返回fileName与baseName避免字符串再次推断。八、一个命名函数应返回结构化结果将路径名和展示名放在一起interfaceExportNames{fileName:string;baseName:string;displayTitle:string;}functioncreateExportNames(pattern:Pattern,now:number):ExportNames{constidsafeSegment(pattern.id,48);constsuffixExportNameFactory.suffix(now);constbaseNamepindouzhitu_${id}_${suffix};return{fileName:${baseName}.png,baseName,displayTitle:pattern.title.trim().length0?pattern.title.trim():拼豆编号图};}传入now而不是函数内部直接读时间有利于固定测试样本。生产调用传Date.now()测试可传确定值并验证完整字符串。若序号工厂也需要可重复测试可以把唯一后缀生成器作为依赖注入而不是依赖静态状态。九、路径拼接还应避免多余分隔符假设当前缓存目录与文件名用/拼接。移动端沙箱路径通常接受这一形式但公共工具函数不应假设cacheDir永远不以分隔符结尾functionjoinCachePath(cacheDir:string,fileName:string):string{constseparatorcacheDir.endsWith(/)?:/;return${cacheDir}${separator}${fileName};}因为fileName已被限制为单个安全片段不允许/或\所以拼接结果不会逃离cacheDir。这条“单片段”不变量比事后检查路径是否包含..更容易证明。如果系统文件 Kit 提供经过目标版本确认的路径拼接 API也可以优先使用无论选择哪种方法都应在进入工具前完成文件名规范化。十、边界样本要覆盖字符、长度和碰撞纯函数样本anime-1 - anime-1 user/generated:1 - user_generated_1 ___ - pattern 中文标题作为 id - pattern 100 个 a - 截到最大长度 同一毫秒调用两次 - 后缀序号不同 空 title - 展示标题回退设备侧再执行连续导出同一图案两次相册出现两个独立资源。构造含/、\、..的 ID缓存文件仍只在cacheDir。用户取消相册对话框后缓存文件被清理。相册展示标题可读扩展名与实际 PNG 一致。文件写入异常时句柄关闭下一次导出仍可用。若文件找不到先输出规范化后的fileName和最终缓存路径若相册标题是一串内部 ID说明展示名仍从文件名推断若连续导出覆盖检查唯一后缀与并发锁是否同时有效。十一、结语安全文件名不是简单删掉斜杠。它需要字符白名单、连续占位折叠、空值回退、长度上限、唯一后缀和格式扩展名共同组成。拼豆制图当前用清洗 ID 与时间戳建立了良好基础也应进一步分离内部路径名和用户可见标题。当命名函数返回结构化结果打包格式、缓存路径和相册元数据都能共享同一份事实。这样既不会让用户标题改变沙箱路径也不会为了路径安全牺牲相册中的可读性。