• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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 UIAbility from '@ohos.app.ability.UIAbility';
17import datafile from '@ohos.file.fileAccess';
18import picker from '@ohos.file.picker';
19import StartOptions from '@ohos.app.ability.StartOptions';
20import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
21import { Permissions } from '@ohos.abilityAccessCtrl';
22import GlobalContext from '../common/GlobalContext';
23import Want from '@ohos.app.ability.Want';
24import common from '@ohos.app.ability.common';
25import AbilityConstant from '@ohos.app.ability.AbilityConstant';
26import window from '@ohos.window';
27import { BusinessError } from '@ohos.base';
28import ability from '@ohos.ability.ability';
29import * as ns from '@ohos.app.ability.UIAbility';
30import dlpPermission from '@ohos.dlpPermission';
31import fs from '@ohos.file.fs';
32import abilityManager from '@ohos.app.ability.abilityManager';
33import { startAlertAbility, getFileUriByPath, getFileFd, isValidPath } from '../common/utils';
34import Constants from '../common/constant';
35import fileAccess from '@ohos.file.fileAccess';
36
37const TAG = '[DLPManager_SaveAs]';
38let permissionList: Array<Permissions> = [
39  'ohos.permission.READ_MEDIA',
40  'ohos.permission.WRITE_MEDIA',
41  'ohos.permission.FILE_ACCESS_MANAGER'
42];
43
44class option_ {
45  offset: number = 0
46  length: number = 0
47}
48
49let defaultDlpFile: dlpPermission.DLPFile = {
50  dlpProperty: {
51    ownerAccount: '',
52    ownerAccountType: GlobalContext.load('domainAccount') as Boolean
53      ? dlpPermission.AccountType.DOMAIN_ACCOUNT : dlpPermission.AccountType.CLOUD_ACCOUNT,
54    authUserList: [],
55    contactAccount: '',
56    offlineAccess: true,
57    ownerAccountID: '',
58    everyoneAccessList: []
59  },
60  recoverDLPFile: async () => {
61  },
62  closeDLPFile: async () => {
63  },
64  addDLPLinkFile: async () => {
65  },
66  stopFuseLink: async () => {
67  },
68  resumeFuseLink: async () => {
69  },
70  replaceDLPLinkFile: async () => {
71  },
72  deleteDLPLinkFile: async () => {
73  }
74};
75
76const SUFFIX_INDEX = 2;
77const HEAD_LENGTH_IN_BYTE = 20;
78const HEAD_LENGTH_IN_U32 = 5;
79const TXT_OFFSET = 3;
80const SIZE_OFFSET = 4;
81const ARGS_ZERO = 0;
82const ARGS_ONE = 1;
83const ARGS_TWO = 2;
84const ACTION: Record<string, string> = {
85  'SELECT_ACTION': 'ohos.want.action.OPEN_FILE',
86  'SELECT_ACTION_MODAL': 'ohos.want.action.OPEN_FILE_SERVICE',
87  'SAVE_ACTION': 'ohos.want.action.CREATE_FILE',
88  'SAVE_ACTION_MODAL': 'ohos.want.action.CREATE_FILE_SERVICE',
89};
90
91const ErrCode: Record<string, number> = {
92  'INVALID_ARGS': 13900020,
93  'RESULT_ERROR': 13900042,
94  'NAME_TOO_LONG': 13900030,
95};
96
97const ERRCODE_MAP = new Map([
98  [ErrCode.INVALID_ARGS, 'Invalid argument'],
99  [ErrCode.RESULT_ERROR, 'Unknown error'],
100  [ErrCode.NAME_TOO_LONG, 'File name too long'],
101]);
102export default class SaveAsAbility extends UIAbility {
103  result: ability.AbilityResult = {
104    resultCode: -1,
105    want: {
106      bundleName: '',
107      abilityName: '',
108      parameters: {
109        pick_path_return: [],
110        pick_fd_return: 0
111      }
112    }
113  };
114  dlpFile: dlpPermission.DLPFile = defaultDlpFile;
115  sandboxBundleName: string = '';
116  resultUri: string = '';
117  tokenId: number = -1;
118  requestCode: number = -1;
119  fileName: string = '';
120  suffix: string = '';
121  authPerm: dlpPermission.DLPFileAccess = dlpPermission.DLPFileAccess.READ_ONLY;
122  isOK: boolean = true; // use with startAlertAbility
123
124  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
125    console.info(TAG, 'onCreate');
126    GlobalContext.store('abilityWant', want);
127    GlobalContext.store('context', this.context);
128    GlobalContext
129      .store('dsHelper', await datafile.createFileAccessHelper(GlobalContext
130        .load('context') as common.UIAbilityContext));
131    try {
132      let atManager = abilityAccessCtrl.createAtManager();
133      await atManager.requestPermissionsFromUser(GlobalContext
134        .load('context') as common.UIAbilityContext, permissionList);
135    } catch (err) {
136      console.error(TAG, 'requestPermissionsFromUser failed', err.code, err.message);
137      return;
138    }
139    await this.prepareDlpFile();
140    try {
141      await abilityManager.notifySaveAsResult(this.result, this.requestCode);
142    } catch (err) {
143      console.error(TAG, 'notifySaveAsResult failed ', (err as BusinessError).code, (err as BusinessError).message);
144    }
145    if (this.isOK === true) {
146      (GlobalContext.load('context') as common.UIAbilityContext).terminateSelf();
147    }
148  }
149
150  onDestroy(): void {
151    console.info(TAG, 'onDestroy');
152  }
153
154  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
155    // Main window is created, set main page for this ability
156    console.info(TAG, 'onWindowStageCreate: ', GlobalContext
157      .load('context') as common.UIAbilityContext);
158
159  }
160
161  onWindowStageDestroy(): void {
162    // Main window is destroyed, release UI related resources
163    console.info(TAG, 'onWindowStageDestroy');
164  }
165
166  onForeground(): void {
167    // Ability has brought to foreground
168    console.info(TAG, 'onForeground');
169  }
170
171  onBackground(): void {
172    // Ability has back to background
173    console.info(TAG, 'onBackground');
174  }
175
176  async parseParams(): Promise<boolean | void> {
177    if (GlobalContext.load('abilityWant') === undefined ||
178    (GlobalContext.load('abilityWant') as Want).parameters === undefined) {
179      console.error(TAG, 'invalid abilityWant');
180      return false;
181    }
182
183    this.requestCode = (GlobalContext.load('abilityWant') as Want)
184      .parameters?.['requestCode'] as number;
185    if (this.requestCode === undefined) {
186      console.error(TAG, 'invalid requestCode');
187      return false;
188    }
189
190    this.tokenId = (GlobalContext.load('abilityWant') as Want)
191      .parameters?.['ohos.aafwk.param.callerToken'] as number;
192    if (this.tokenId === undefined) {
193      console.error(TAG, 'invalid tokenId');
194      return false;
195    }
196
197    this.authPerm = (GlobalContext.load('token2File') as
198    Map<number, (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[]>)
199      .get(this.tokenId)?.[3] as dlpPermission.DLPFileAccess;
200
201    if (this.authPerm != dlpPermission.DLPFileAccess.CONTENT_EDIT
202    && this.authPerm != dlpPermission.DLPFileAccess.FULL_CONTROL) {
203      console.error(TAG, 'invalid authPerm ', this.authPerm);
204      this.isOK = false;
205      await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext,
206        {
207          code: Constants.ERR_JS_DLP_FILE_READ_ONLY
208        } as BusinessError);
209      return false;
210    }
211    if (!(GlobalContext.load('token2File') as Map<number, Object[]>).has(this.tokenId)) {
212      console.error(TAG, 'invalid token2File');
213      return;
214    }
215
216    this.fileName = (GlobalContext.load('abilityWant') as Want)
217      .parameters?.['key_pick_file_name'] as string;
218    if (this.fileName === undefined) {
219      console.error(TAG, 'invalid fileName');
220      return false;
221    }
222    this.fileName = String(this.fileName);
223
224    let splitNames = this.fileName.split('.');
225    console.debug(TAG, 'splitNames:', splitNames);
226    if (splitNames.length <= SUFFIX_INDEX) {
227      console.error(TAG, 'get suffix failed');
228      return;
229    }
230    this.suffix = splitNames[splitNames.length - SUFFIX_INDEX];
231    console.info(TAG, 'suffix is', this.suffix);
232    return true;
233  }
234
235  async copyDlpHead(srcFd: number, dstFd: number): Promise<boolean> {
236    try {
237      let z = new ArrayBuffer(HEAD_LENGTH_IN_BYTE);
238      let option: option_ = {
239        offset: 0,
240        length: HEAD_LENGTH_IN_BYTE
241      };
242      let num = fs.readSync(srcFd, z, option);
243      let buf = new Uint32Array(z, 0, HEAD_LENGTH_IN_U32);
244
245      let txtOffset = buf[TXT_OFFSET];
246      let head = new ArrayBuffer(txtOffset);
247      option = {
248        offset: 0,
249        length: txtOffset
250      };
251      num = fs.readSync(srcFd, head, option);
252      let buf2 = new Uint32Array(head, 0, HEAD_LENGTH_IN_U32);
253      buf2[SIZE_OFFSET] = 0;
254      num = fs.writeSync(dstFd, head, option);
255    } catch (err) {
256      console.error(TAG, 'copyDlpHead ', (err as BusinessError).code, (err as BusinessError).message);
257      return false;
258    }
259
260    return true;
261  }
262
263  parseDocumentPickerSaveOption(args: picker.DocumentSaveOptions[], action: string) {
264    let config: Record<string, string | Record<string, Object>> = {
265      'action': action,
266      'parameters': {
267        'startMode': 'save',
268      } as Record<string, Object>
269    };
270
271    if (args.length > ARGS_ZERO && typeof args[ARGS_ZERO] === 'object') {
272      let option: picker.DocumentSaveOptions = args[ARGS_ZERO];
273      if ((option.newFileNames !== undefined) && option.newFileNames.length > 0) {
274        config.parameters['key_pick_file_name'] = option.newFileNames;
275        config.parameters['saveFile'] = option.newFileNames[0];
276      }
277
278      if (option.defaultFilePathUri !== undefined) {
279        config.parameters['key_pick_dir_path'] = option.defaultFilePathUri;
280      }
281      if ((option.fileSuffixChoices !== undefined) && option.fileSuffixChoices.length > 0) {
282        config.parameters['key_file_suffix_choices'] = option.fileSuffixChoices;
283      }
284    }
285
286    console.log(TAG, '[picker] Save config: ' + JSON.stringify(config));
287    return config;
288  }
289
290  getDocumentPickerSaveResult(args: ability.AbilityResult) {
291    let saveResult: Record<string, BusinessError | string[]> = {
292      'error': {} as BusinessError,
293      'data': []
294    };
295
296    if ((args.resultCode !== undefined && args.resultCode === 0)) {
297      if (args.want && args.want.parameters) {
298        saveResult.data = args.want.parameters.pick_path_return as string[];
299      }
300    } else if ((args.resultCode !== undefined && args.resultCode === -1)) {
301      saveResult.data = [];
302    } else {
303      saveResult.error = this.getErr(ErrCode.RESULT_ERROR) as BusinessError;
304    }
305
306    console.log(TAG, '[picker] Save saveResult: ' + JSON.stringify(saveResult));
307    return saveResult;
308  }
309
310  getErr(errCode: number) {
311    return {code: errCode, message: ERRCODE_MAP.get(errCode)} as BusinessError;
312  }
313  async documentPickerSave(...args: Object[]): Promise<BusinessError | string[] | undefined>  {
314    let context = GlobalContext.load('context') as common.UIAbilityContext;
315    let config: Record<string, string | Record<string, Object>>;
316    let result: ability.AbilityResult;
317    try {
318      config = this.parseDocumentPickerSaveOption(args, ACTION.SAVE_ACTION_MODAL);
319      config = this.parseDocumentPickerSaveOption(args, ACTION.SAVE_ACTION);
320      result = await context.startAbilityForResult(config, {windowMode: 0});
321    } catch (error) {
322      console.log(TAG, '[picker] error: ' + error);
323      return undefined;
324    }
325    console.log(TAG, '[picker] Save result: ' + JSON.stringify(result));
326    try {
327      const saveResult: Record<string, BusinessError | string[]> = this.getDocumentPickerSaveResult(result);
328      if (args.length === ARGS_TWO && typeof args[ARGS_ONE] === 'function') {
329        return (args[ARGS_ONE] as Function)(saveResult.error, saveResult.data);
330      } else if (args.length === ARGS_ONE && typeof args[ARGS_ZERO] === 'function') {
331        return (args[ARGS_ZERO] as Function)(saveResult.error, saveResult.data);
332      }
333      return new Promise<BusinessError | string[]>((resolve, reject) => {
334        if (saveResult.data !== undefined) {
335          resolve(saveResult.data);
336        } else {
337          reject(saveResult.error);
338        }
339      })
340    } catch (resultError) {
341      console.log(TAG, '[picker] Result error: ' + resultError);
342    }
343    return undefined;
344  }
345
346  async prepareDlpFile(): Promise<void> {
347    console.info(TAG, 'getFile start:', JSON.stringify(GlobalContext.load('abilityWant')));
348    let uri = '';
349    let displayName = '';
350
351    let ret = await this.parseParams();
352    if (!ret) {
353      return;
354    }
355    let DocumentSaveOptions = new picker.DocumentSaveOptions();
356    displayName = this.fileName;
357    DocumentSaveOptions.newFileNames = [displayName];
358    let documentPicker = new picker.DocumentViewPicker();
359    let dstFd: number;
360    let file: fs.File | undefined;
361
362    try {
363
364      let saveRes: BusinessError | string[] | undefined = await this.documentPickerSave(DocumentSaveOptions);
365      if (saveRes == undefined || (saveRes instanceof Array && saveRes.length == 0)) {
366        console.error(TAG, 'fail to get uri');
367        return;
368      }
369      console.info(TAG, 'get uri', saveRes)
370      uri = saveRes[0]
371      if (!isValidPath(uri)){
372        console.error(TAG, 'invalid uri');
373        return;
374      }
375      try {
376        file = await fs.open(uri, fs.OpenMode.READ_WRITE);
377        dstFd = file.fd;
378      } catch (err) {
379        console.error(TAG, 'open', uri, 'failed', (err as BusinessError).code, (err as BusinessError).message);
380        try {
381          if (file != undefined) {
382            await fs.close(file);
383          }
384        } catch (err) {
385          console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message);
386        }
387        await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri);
388        this.isOK = false;
389        await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext,
390          {
391            code: Constants.ERR_JS_APP_INSIDE_ERROR
392          } as BusinessError);
393        return;
394      }
395
396      let token2File_value: (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[]
397        = (GlobalContext
398        .load('token2File') as Map<number, (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[]>)
399        .get(this.tokenId) as (number | string | dlpPermission.DLPFile | dlpPermission.DLPFileAccess)[];
400      this.dlpFile = token2File_value[0] as dlpPermission.DLPFile;
401      this.sandboxBundleName = token2File_value[1] as string;
402      let appId: number = token2File_value[2] as number;
403      let srcUri: string = token2File_value[4] as string;
404
405      let srcFd = getFileFd(srcUri);
406      let copyRes = await this.copyDlpHead(srcFd, dstFd);
407      if (!copyRes) {
408        try {
409          await fs.close(file);
410        } catch (err) {
411          console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message);
412        }
413        await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri);
414        fs.closeSync(srcFd);
415        this.isOK = false;
416        await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext,
417          {
418            code: Constants.ERR_JS_APP_INSIDE_ERROR
419          } as BusinessError);
420        return;
421      }
422      let newDlpFile: dlpPermission.DLPFile;
423      try {
424        newDlpFile = await dlpPermission.openDLPFile(dstFd);
425      } catch (err) {
426        console.error(TAG, 'generateDlpFile', dstFd, 'failed', (err as BusinessError).code, (err as BusinessError).message);
427        try {
428          await fs.close(file);
429        } catch (err) {
430          console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message);
431        }
432        await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri);
433        fs.closeSync(srcFd);
434        return;
435      }
436
437      let date = new Date();
438      let timestamp = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(),
439        date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getMilliseconds()).getTime();
440
441      let linkFileName = String(this.sandboxBundleName).substring(0, Constants.BUNDLE_LEN) + '_' + appId +
442      '_' + timestamp + String(Math.random()).substring(Constants.RAND_START, Constants.RAND_END) + '.' +
443      this.suffix + '.dlp.link';
444
445      try {
446        await newDlpFile.addDLPLinkFile(linkFileName);
447      } catch (err) {
448        console.error(TAG, 'addDlpLinkFile failed', (err as BusinessError).code, (err as BusinessError).message);
449        try {
450          await newDlpFile.closeDLPFile();
451        } catch (err) {
452          console.error(TAG, 'closeDlpFile failed', (err as BusinessError).code, (err as BusinessError).message);
453        }
454        try {
455          await fs.close(file);
456        } catch (err) {
457          console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message);
458        }
459        await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri);
460        fs.closeSync(srcFd);
461        this.isOK = false;
462        await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext,
463          {
464            code: Constants.ERR_JS_APP_INSIDE_ERROR
465          } as BusinessError);
466        return;
467      }
468
469      let linkFilePath = Constants.FUSE_PATH + linkFileName;
470      let linkUri = getFileUriByPath(linkFilePath);
471      (GlobalContext.load("token2File") as Map<number, Object[]>)
472        .set(this.tokenId, [this.dlpFile, this.sandboxBundleName, appId, this.authPerm, srcUri]);
473      let sandbox2linkFile: Map<string, (number | string | dlpPermission.DLPFile)[][]>
474        = GlobalContext
475        .load('sandbox2linkFile') as Map<string, (number | string | dlpPermission.DLPFile)[][]>;
476      sandbox2linkFile.get(this.sandboxBundleName + appId)?.push([newDlpFile, linkFileName, dstFd, this.tokenId]);
477
478      (GlobalContext.load("fileOpenHistory") as Map<string, Object[]>)
479        .set(uri, [this.sandboxBundleName, appId, linkFileName, linkUri]);
480
481      (GlobalContext.load("linkSet") as Set<string>).add(linkUri);
482
483      this.resultUri = getFileUriByPath(linkFilePath);
484
485      console.info(TAG, 'result uri is', this.resultUri);
486
487      (this.result.want?.parameters?.pick_path_return as string[]).push(this.resultUri);
488      this.result.resultCode = 0;
489      fs.closeSync(srcFd);
490      return;
491    } catch (err) {
492      console.error(TAG, 'DocumentViewPicker failed', (err as BusinessError).code, (err as BusinessError).message);
493      try {
494        if (file != undefined) {
495          await fs.close(file);
496        }
497      } catch (err) {
498        console.log(TAG, 'close fail', (err as BusinessError).code, (err as BusinessError).message);
499      }
500      await (GlobalContext.load('dsHelper') as fileAccess.FileAccessHelper).delete(uri);
501      this.isOK = false;
502      await startAlertAbility(GlobalContext.load('context') as common.UIAbilityContext,
503        {
504          code: Constants.ERR_JS_APP_INSIDE_ERROR
505        } as BusinessError);
506      return;
507    }
508  }
509
510};
511