1/*
2 * Copyright (c) 2021-2023 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { fileTree } from './component/dialog/FileMoveDialog';
17import Logger from '../base/log/Logger';
18import ErrorCodeConst from '../base//constants/ErrorCodeConst';
19import { toast } from '../base/utils/Common';
20import AbilityCommonUtil, { ResultCodePicker } from '../base/utils/AbilityCommonUtil';
21import { SYSTEM_BAR_COLOR } from '../base/constants/UiConstant';
22import StringUtil from '../base/utils/StringUtil';
23import { FileUtil } from '../base/utils/FileUtil';
24import ObjectUtil from '../base/utils/ObjectUtil';
25import fileAccess from '@ohos.file.fileAccess';
26import { ArrayUtil } from '../base/utils/ArrayUtil';
27import { UiUtil } from '../base/utils/UiUtil';
28import { StartModeOptions } from '../base/model/StartModeOptions';
29import { FilePickerUtil } from '../base/utils/FilePickerUtil';
30import { photoAccessHelper } from '@kit.MediaLibraryKit';
31
32const TAG = 'PathSelector';
33let storage = LocalStorage.getShared();
34
35@Entry(storage)
36@Component
37struct PathSelector {
38  private startModeOptions: StartModeOptions = FilePickerUtil.getStartOptionsFromStorage();
39  @State createResultType: number = ErrorCodeConst.PICKER.NORMAL;
40
41  aboutToAppear() {
42    UiUtil.setWindowBackground(SYSTEM_BAR_COLOR.LIGHT_GRAY);
43  }
44
45  async saveFileCallback(res, startModeOptions: StartModeOptions): Promise<void> {
46    if (res?.cancel) {
47      AbilityCommonUtil.terminatePathPicker([], ResultCodePicker.CANCEL, startModeOptions);
48      return;
49    } else {
50      let fileNameList = this.startModeOptions.newFileNames;
51      // 保存单个文件时文件名可修改,需使用修改后的文件名来创建文件
52      if (fileNameList.length <= 1) {
53        fileNameList = [res.fileName];
54      }
55      this.saveFiles(res.selectUri, fileNameList).then((createdFileList) => {
56        AbilityCommonUtil.terminatePathPicker(createdFileList, ResultCodePicker.SUCCESS, startModeOptions);
57      }).catch((err) => {
58        let errorMessage = '';
59        let errorCode = 0;
60        Logger.e(TAG, JSON.stringify(err));
61        if (err.code) {
62          if (err.code === ErrorCodeConst.FILE_ACCESS.FILE_NAME_EXIST) {
63            errorMessage = 'Same name file already exists';
64            errorCode = ErrorCodeConst.PICKER.FILE_NAME_EXIST;
65            this.createResultType = errorCode;
66            const pathName = startModeOptions.newFileNames;
67            let listLength: number = pathName.length;
68            if (listLength == 1) {
69              return;
70            }
71          } else if (err.code === ErrorCodeConst.FILE_ACCESS.FILE_NAME_INVALID) {
72            errorMessage = 'Invalid display name';
73            errorCode = ErrorCodeConst.PICKER.FILE_NAME_INVALID;
74          } else {
75            errorMessage = 'File create failed';
76            errorCode = ErrorCodeConst.PICKER.OTHER_ERROR;
77          }
78        } else {
79          errorMessage = err.message ? err.message : err;
80          errorCode = ErrorCodeConst.PICKER.OTHER_ERROR;
81        }
82        AbilityCommonUtil.terminatePathPicker([], errorCode, startModeOptions);
83        toast($r('app.string.save_file_fail'));
84        Logger.e(TAG, `path select error, errorCode: ${errorCode}, errorMessage: ${errorMessage}`);
85      })
86    }
87  }
88
89  /**
90   * PathPicker保存文件
91   * @param data SaveFilesParam
92   */
93  async saveFiles(path: string, nameList: string[]): Promise<string[]> {
94    return new Promise(async (resolve, reject) => {
95      let fileAccessHelper = await FileUtil.getFileAccessHelperAsync(globalThis.abilityContext);
96      let dirPath = path;
97      if (StringUtil.isEmpty(dirPath)) {
98        dirPath = (await FileUtil.getFileInfoByRelativePath('Documents/', fileAccessHelper)).uri;
99      }
100      let fileNameArr = nameList;
101      let successArr: string[] = [];
102      let resultErr: any;
103      let len: number = fileNameArr.length;
104      let fileNameList: string[] = [];
105      if (len > 1) {
106        fileNameList = await this.getPickPathListFiles(dirPath, fileAccessHelper);
107      }
108      Logger.i(TAG, 'saveFiles createName: ' + JSON.stringify(fileNameArr) + ' ; ');
109      Logger.i(TAG, 'saveFiles subList: ' + JSON.stringify(fileNameList) + ' ; ');
110      for (let i = 0; i < len; i++) {
111        const currName = fileNameArr[i];
112        let result;
113        if (len === 1) {
114          result = await FileUtil.createFile(fileAccessHelper, dirPath, currName);
115        } else {
116          result = await this.tryRenameFileOperate(fileAccessHelper, currName, dirPath, 0, fileNameList);
117        }
118        if (ObjectUtil.isUndefined(result.err)) {
119          Logger.i(TAG, 'saveFiles createOK: ' + result.uri);
120          successArr.push(result.uri);
121          continue;
122        }
123        Logger.i(TAG, 'saveFiles err: ' + result.err.code);
124        // 失败
125        resultErr = { code: result.err.code, message: result.err.message };
126        let photoManageHelper: photoAccessHelper.PhotoAccessHelper = AbilityCommonUtil.getPhotoManageHelper();
127        if (ObjectUtil.isNullOrUndefined(photoManageHelper)) {
128          break;
129        }
130        for (let i = 0; i < successArr.length; i++) {
131          await FileUtil.hardDelete(successArr[i]);
132        }
133        try {
134          photoManageHelper.release();
135        } catch (e) {
136          Logger.e(TAG, 'mediaLibrary close error');
137        }
138        successArr = [];
139        break;
140      }
141
142      Logger.i(TAG, 'saveFiles end: ' + JSON.stringify(successArr));
143      if (!ArrayUtil.isEmpty(successArr)) {
144        resolve(successArr);
145      } else {
146        reject(resultErr);
147      }
148    })
149  }
150
151  private async getPickPathListFiles(dirUri: string, fileAccessHelper: fileAccess.FileAccessHelper): Promise<string[]> {
152    let fileInfo: fileAccess.FileInfo = await FileUtil.getFileInfoByUri(dirUri, fileAccessHelper);
153    if (ObjectUtil.isNullOrUndefined(fileInfo) || !FileUtil.isFolder(fileInfo.mode)) {
154      return [];
155    }
156    return this.getFilesByIterator(fileInfo.listFile());
157  }
158
159  private getFilesByIterator(fileIterator: fileAccess.FileIterator): string[] {
160    if (ObjectUtil.isNull(fileIterator)) {
161      return null;
162    }
163    let result: string[] = [];
164    let isDone = false;
165    while (!isDone) {
166      try {
167        let nextFileInfo = fileIterator.next();
168        isDone = nextFileInfo.done;
169        if (isDone) {
170          break;
171        }
172        let currFile = nextFileInfo.value;
173        if (!FileUtil.isFolder(currFile.mode)) {
174          result.push(currFile.fileName);
175        }
176      } catch (err) {
177        Logger.e(TAG, 'current File err: ' + JSON.stringify(err) + ', ' + err.toString());
178      }
179    }
180    return result;
181  }
182
183  private async tryRenameFileOperate(fileAccessHelper: fileAccess.FileAccessHelper, fileName: string,
184    dirUri: string, renameCount: number, fileNameList: string[] = []): Promise<{
185    err,
186    uri
187  }> {
188    let index = fileName.lastIndexOf('.');
189    let name = fileName;
190    let suffix = '';
191    if (index !== -1) {
192      suffix = fileName.substring(index, fileName.length);
193      name = fileName.substring(0, index);
194    }
195    let hasReNameCount = FileUtil.getFileNameReName(name);
196    if (!ObjectUtil.isNullOrUndefined(hasReNameCount)) {
197      let num = Number(hasReNameCount[1]);
198      if (!isNaN(num)) {
199        name = hasReNameCount[0];
200        renameCount = num;
201      }
202    }
203
204    let newName = fileName;
205    while (true) {
206      newName = FileUtil.renameFile(name, renameCount++, suffix);
207      let index = this.getIndex(newName, fileNameList);
208      Logger.i(TAG, 'tryRenameFileOperate : ' + newName + ' ; index = ' + index);
209      if (index === -1) {
210        const result = await FileUtil.createFile(fileAccessHelper, dirUri, newName);
211        if (ObjectUtil.isUndefined(result.err)) {
212          Logger.i(TAG, 'tryRenameFileOperate createOK: ' + result.uri);
213          return result;
214        } else {
215          Logger.i(TAG, 'tryRenameFileOperate createFail: ' + JSON.stringify(result) + ' ; ' + newName);
216          if (result.err.code === ErrorCodeConst.FILE_ACCESS.FILE_NAME_EXIST) {
217            fileNameList.push(newName);
218          } else {
219            return result;
220          }
221        }
222      }
223    }
224  }
225
226  private getIndex(fileName: string, fileNameList: string[] = []) {
227    return fileNameList.findIndex(value => value === fileName);
228  }
229
230  build() {
231    if (this.startModeOptions.isUxt()) {
232      Column() {
233      }.bindSheet(true, this.mainContent(), {
234        height: '95%',
235        dragBar: false,
236        showClose: false,
237        preferType: SheetType.CENTER,
238        onAppear: () => {
239        },
240        shouldDismiss: () => {
241          this.startModeOptions.session.terminateSelf();
242        }
243      })
244    } else {
245      this.mainContent()
246    }
247  }
248
249  @Builder
250  mainContent() {
251    Row() {
252      fileTree({
253        startModeOptions: this.startModeOptions,
254        createFileFailType: $createResultType,
255        moveCallback: (e) => {
256          this.saveFileCallback(e, this.startModeOptions);
257        }
258      })
259    }
260  }
261}
262