已开启
fix(ohos): 等待图片压缩完成后再拉起相册保存框 #10
fix(ohos): 等待图片压缩完成后再拉起相册保存框 #10
已开启
李唐创建于 8月25日
共 1 个文件变更+95-34
@@ -88,51 +88,92 @@ export default class ImageGallerySaverPlusPlugin implements FlutterPlugin, Metho
88 async onMethodCall(call: MethodCall, result: MethodResult) {88 async onMethodCall(call: MethodCall, result: MethodResult) {
89 switch (call.method) {89 switch (call.method) {
90 case "saveFileToGallery":90 case "saveFileToGallery":
91- if (this.noFirstInvoke) {91+ await this.runExclusiveSave(result, () => {
92- return;92+ let fileName: string = call.argument("name");
93- }93+ let uri: string = call.argument("file");
94- this.noFirstInvoke = true;94+ return this.saveFileToGallery(fileName, uri);
95- let FileName: string = call.argument("name");95+ });
96- let uri: string = call.argument("file");
97- result.success(await this.saveFileToGallery(FileName, uri));
98- this.noFirstInvoke = false;
99 break;96 break;
100 case "saveImageToGallery":97 case "saveImageToGallery":
101- if (this.noFirstInvoke) {98+ await this.runExclusiveSave(result, () => {
102- return;99+ let imageByte: Uint8Array = call.argument("imageBytes");
103- }100+ let quality: number = call.argument("quality");
104- this.noFirstInvoke = true;101+ let imageName: string = call.argument("name");
105- let imageByte: Uint8Array = call.argument("imageBytes");102+ return this.saveImageToGallery(imageByte, quality, imageName);
106- let quality: number = call.argument("quality");103+ });
107- let ImageName: string = call.argument("name");
108- result.success(await this.saveImageToGallery(imageByte, quality, ImageName));
109- this.noFirstInvoke = false;
110 break;104 break;
111 default:105 default:
112 result.notImplemented();106 result.notImplemented();
113 }107 }
114 }108 }
115 109 
110+ private async runExclusiveSave(
111+ result: MethodResult,
112+ action: () => Promise<ResultMessage>
113+ ): Promise<void> {
114+ if (this.noFirstInvoke) {
115+ result.success(new ResultMessage(null, "save in progress", false));
116+ return;
117+ }
118+ this.noFirstInvoke = true;
119+ try {
120+ result.success(await action());
121+ } catch (err) {
122+ const e = err as BusinessError;
123+ result.success(new ResultMessage(null,
124+ `save failed, errCode is ${e.code}, errMsg is ${e.message}`, false));
125+ } finally {
126+ this.noFirstInvoke = false;
127+ }
128+ }
129+ 
116 // 保存图片130 // 保存图片
117 async saveImageToGallery(imageByte: Uint8Array, quality: number, name: string): Promise<ResultMessage> {131 async saveImageToGallery(imageByte: Uint8Array, quality: number, name: string): Promise<ResultMessage> {
118 if (this.context == null || this.context == undefined) {132 if (this.context == null || this.context == undefined) {
119 return new ResultMessage(null, "applicationContext is null", false);133 return new ResultMessage(null, "applicationContext is null", false);
120 }134 }
121- let imageSource: image.ImageSource = image.createImageSource(imageByte.buffer);135+ if (imageByte == null || imageByte.length === 0) {
122- let packOpts: image.PackingOption = { format: "image/png", quality: quality };136+ return new ResultMessage(null, "parameters error", false);
123- const filePath: string = this.context?.filesDir + "/imageLocal.png";137+ }
124- let file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);138+ 
125- const imagePackerObj: image.ImagePacker = image.createImagePacker();139+ const title = this.sanitizeTitle(name);
126- imagePackerObj.packToFile(imageSource, file.fd, packOpts, (err: BusinessError) => {140+ const filePath: string = `${this.context.filesDir}/image_${Date.now()}.png`;
cpf-manager
cpf-managercpf-manager8月25日

【AI-Review】【一般】【基础代码问题】【资源使用问题】临时文件未清理导致 filesDir 持续膨胀

● 问题:saveImageToGallery 用 image_${Date.now()}.png 生成唯一临时文件名(line 140),packToFile 写入数据,finally 仅 closeSync 关闭文件(line 169-171),随后 saveToPhoto 通过 fs.copyFile 复制到相册(saveToPhoto line 225-227),但复制完成后仅关闭 srcFile/desFile,未调用 fs.unlink 删除源文件。每次成功保存图片后,filesDir 下都会残留一个与原图等大的临时 png 文件。对比原代码固定使用 imageLocal.png(每次覆盖)的模式,本次变更为唯一文件名后引入了累积问题。

● 影响:一般。每次保存图片后会在 filesDir 留下与原图大小相当的临时文件(通常几 MB),频繁保存图片会导致 filesDir 持续膨胀,长期占用用户存储空间,且无清理机制。

● 建议:在 saveToPhoto 成功复制后清理临时文件,可在 line 182 调用 saveToPhoto 之后增加清理: const result = await this.saveToPhoto(filePath, photoCreationConfigs); try { fs.unlinkSync(filePath); } catch (_) {} return result; 或在 saveImageToGallery 的 finally 块中追加文件删除逻辑(注意 packToFile 失败分支也要清理空文件)。

likedislike
127- if (err) {141+ let file: fs.File | null = null;
128- console.error(`Failed to pack the image to file.code ${err.code},message is ${err.message}`);142+ let imageSource: image.ImageSource | null = null;
129- } else {143+ let imagePackerObj: image.ImagePacker | null = null;
130- console.info('Succeeded in packing the image to file.');144+ try {
145+ file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC);
146+ imageSource = image.createImageSource(imageByte.buffer);
147+ imagePackerObj = image.createImagePacker();
148+ const packOpts: image.PackingOption = { format: "image/png", quality: quality ?? 80 };
149+ await new Promise<void>((resolve, reject) => {
150+ imagePackerObj!.packToFile(imageSource, file!.fd, packOpts, (err: BusinessError) => {
151+ if (err) {
152+ reject(err);
153+ } else {
154+ resolve();
155+ }
156+ });
157+ });
158+ } catch (err) {
159+ const e = err as BusinessError;
160+ return new ResultMessage(null,
161+ `Failed to pack the image to file.code ${e.code},message is ${e.message}`, false);
162+ } finally {
163+ if (imagePackerObj) {
164+ imagePackerObj.release();
131 }165 }
132- })166+ if (imageSource) {
167+ imageSource.release();
168+ }
169+ if (file) {
170+ fs.closeSync(file);
171+ }
172+ }
173+ 
133 let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [174 let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
134 {175 {
135- title: name,176+ title: title,
136 fileNameExtension: "png",177 fileNameExtension: "png",
137 photoType: photoAccessHelper.PhotoType.IMAGE,178 photoType: photoAccessHelper.PhotoType.IMAGE,
138 subtype: photoAccessHelper.PhotoSubtype.DEFAULT,179 subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
@@ -157,7 +198,7 @@ export default class ImageGallerySaverPlusPlugin implements FlutterPlugin, Metho
157 }198 }
158 let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [199 let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
159 {200 {
160- title: this.savefileName,201+ title: this.sanitizeTitle(this.savefileName),
161 fileNameExtension: this.savefileType,202 fileNameExtension: this.savefileType,
162 photoType: this.isImage ? photoAccessHelper.PhotoType.IMAGE : photoAccessHelper.PhotoType.VIDEO,203 photoType: this.isImage ? photoAccessHelper.PhotoType.IMAGE : photoAccessHelper.PhotoType.VIDEO,
163 subtype: photoAccessHelper.PhotoSubtype.DEFAULT,204 subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
@@ -173,7 +214,7 @@ export default class ImageGallerySaverPlusPlugin implements FlutterPlugin, Metho
173 fileUri.getUriFromPath(uri)214 fileUri.getUriFromPath(uri)
174 ];215 ];
175 let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);216 let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
176- // 拉起弹窗217+ // 拉起系统保存弹窗;用户取消时返回空数组
177 let desFileUris: Array<string> = await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);218 let desFileUris: Array<string> = await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
178 if (desFileUris.length == 0) {219 if (desFileUris.length == 0) {
179 return new ResultMessage(null, "user refuses permission", false);220 return new ResultMessage(null, "user refuses permission", false);
@@ -207,8 +248,8 @@ export default class ImageGallerySaverPlusPlugin implements FlutterPlugin, Metho
207 let fileName: string[];248 let fileName: string[];
208 let result = uri.split("/");249 let result = uri.split("/");
209 fileName = result[result.length-1].split(".");250 fileName = result[result.length-1].split(".");
210- this.savefileName = fileName[0].toLowerCase();251+ this.savefileName = fileName[0] ? fileName[0].toLowerCase() : '';
211- this.savefileType = fileName[1].toLowerCase();252+ this.savefileType = fileName.length > 1 ? fileName[fileName.length - 1].toLowerCase() : '';
212 if (name !== null && name !== undefined) {253 if (name !== null && name !== undefined) {
213 fileName = name.split(".");254 fileName = name.split(".");
214 switch (fileName.length) {255 switch (fileName.length) {
@@ -233,4 +274,24 @@ export default class ImageGallerySaverPlusPlugin implements FlutterPlugin, Metho
233 }274 }
234 return flag275 return flag
235 }276 }
236-}277+ 
278+ // 鸿蒙相册 title 不能含 ':' 等字符,空 title 可能导致保存框不返回
279+ private sanitizeTitle(name: string | null | undefined): string {
280+ let raw = '';
281+ if (name !== null && name !== undefined && name.length > 0) {
282+ raw = name.split('.')[0];
283+ }
284+ let cleaned = '';
285+ for (let i = 0; i < raw.length; i++) {
286+ const ch = raw.charAt(i);
287+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
288+ (ch >= '0' && ch <= '9') || ch === '_' || ch === '-') {
289+ cleaned += ch;
290+ }
291+ }
292+ if (cleaned.length > 0) {
293+ return cleaned;
294+ }
295+ return `image_${Date.now()}`;
296+ }
297+}