diff --git a/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/LongStringStorage.ets b/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/LongStringStorage.ets new file mode 100644 index 0000000000000000000000000000000000000000..ffb7dbab7251edfd0c3f9c4f16f5c392b812647b --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/LongStringStorage.ets @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023 Hunan OpenValley Digital Industry Development Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BusinessError } from '@ohos.base'; +import fs from '@ohos.file.fs'; +import Log from '@ohos/flutter_ohos/src/main/ets/util/Log'; + +const PREFERENCES_LONG_STRING_DIR = 'preferences_long_string'; +const TAG = "SharedPreferencesOhosPlugin"; + +export class LongStringStorage { + /** + * The root directory for file storage. + */ + static rootDirectory: string; + /** + * Flag to determine whether to throw an error on failure. + */ + static needThrowError = false; + + static inUse = false; + + /** + * Initializes the root directory for file storage. + * + * @param {Context} context - The application context. + */ + static async init(context: Context): Promise { + LongStringStorage.rootDirectory = `${context.filesDir}/${PREFERENCES_LONG_STRING_DIR}`; + let exists: boolean = await fs.access(LongStringStorage.rootDirectory, fs.AccessModeType.EXIST); + if (!exists) { + return; + } + + let files = await fs.listFile(LongStringStorage.rootDirectory); + LongStringStorage.inUse = files.length > 0 + } + + /** + * Stores a long string in a specified file. + * + * @param {string} fileName - The name of the file. + * @param {string} longString - The string to be stored. + * @returns {Promise} A promise that resolves with the file path if successful, or null if an error occurs. + * @throws {Error} Throws an error if writing to the file fails and LongStringStorage.needThrowError is true. + */ + static async put(fileName: string, longString: string): Promise { + LongStringStorage.inUse = true; + await LongStringStorage.createStorageDirectory(); + const filePath = `${LongStringStorage.rootDirectory}/${fileName}`; + await LongStringStorage.delete(filePath); + try { + const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + await new Promise((resolve, reject) => { + fs.write(file.fd, longString, (err: BusinessError, writeLen: number) => { + fs.closeSync(file); + if (err) { + let errorInfo = `Failed to write data to file. Error: ${err.message}, Code: ${err.code}`; + Log.e(TAG, errorInfo); + if (LongStringStorage.needThrowError) { + throw new Error(errorInfo); + } else { + reject(err); + } + } else { + Log.i(TAG, `Data successfully written to file. Size: ${writeLen}`); + resolve(); + } + }); + }); + return filePath; + } catch (err) { + let errorInfo = `Failed to store long string in file ${fileName}: ${err.message}`; + Log.e(TAG, errorInfo); + if (LongStringStorage.needThrowError) { + throw new Error(errorInfo); + } + return null; + } + } + + /** + * Retrieves the content of a specified file. + * + * @param {string} filePath - The path of the file. + * @returns {Promise} A promise that resolves with the file content or undefined if an error occurs. + * @throws {Error} Throws an error if reading from the file fails and LongStringStorage.needThrowError is true. + */ + static async get(filePath: string): Promise { + let exists: boolean = await fs.access(filePath, fs.AccessModeType.EXIST); + if (!exists) { + return undefined; + } + try { + const content = await fs.readText(filePath); + Log.i(TAG, `Successfully read text: ${content}`); + return content; + } catch (err) { + let errorInfo = `Failed to read text from file. Error: ${err.message}, Code: ${err.code}`; + Log.e(TAG, errorInfo); + if (LongStringStorage.needThrowError) { + throw new Error(errorInfo); + } else { + return undefined; + } + } + } + + /** + * Creates the root directory for file storage if it doesn't exist. + * + * @returns {Promise} A promise that resolves when the directory is created or already exists. + * @throws {Error} Throws an error if creating the directory fails and LongStringStorage.needThrowError is true. + */ + static async createStorageDirectory(): Promise { + if (LongStringStorage.rootDirectory == null) { + let errorInfo = 'Root directory does not exist. Please check if the context is correctly associated with the engine.'; + Log.e(TAG, errorInfo); + if (LongStringStorage.needThrowError) { + throw new Error(errorInfo); + } + } + let exists: boolean = await fs.access(LongStringStorage.rootDirectory, fs.AccessModeType.EXIST); + if (exists) { + return; + } + try { + await fs.mkdir(LongStringStorage.rootDirectory, true); + Log.i(TAG, "Directory successfully created"); + } catch (err) { + let errorInfo = `Failed to create directory. Error: ${err.message}, Code: ${err.code}`; + Log.e(TAG, errorInfo); + if (LongStringStorage.needThrowError) { + throw new Error(errorInfo); + } + } + } + + /** + * Deletes a specified file from storage. + * + * @param {string} filePath - The path of the file to be deleted. + * @returns {Promise} A promise that resolves with true if the file was deleted, or false if the file does not exist or an error occurs. + * @throws {Error} Throws an error if deleting the file fails and LongStringStorage.needThrowError is true. + */ + static async delete(filePath: string): Promise { + let exists: boolean = await fs.access(filePath, fs.AccessModeType.EXIST); + if (!exists) { + Log.w(TAG, `Failed to delete. The file does not exist: ${filePath}`); + return false; + } + + return fs.unlink(filePath).then(() => { + Log.i(TAG, `Successfully removed file: ${filePath}`); + return true; + }).catch((err: BusinessError) => { + let errorInfo = `Failed to remove file. Error: ${err.message}, Code: ${err.code}`; + Log.e(TAG, errorInfo); + if (LongStringStorage.needThrowError) { + throw new Error(errorInfo); + } else { + return false; + } + }); + } +} diff --git a/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/SharedPreferencesOhosPlugin.ets b/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/SharedPreferencesOhosPlugin.ets index 8f53019ca13fafb0bc7ddcb8247c4a1be6824516..4fa2284f5c84210b2845d7c61a1ec3492061bb67 100644 --- a/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/SharedPreferencesOhosPlugin.ets +++ b/packages/shared_preferences/shared_preferences_ohos/ohos/src/main/ets/shared_preferences/SharedPreferencesOhosPlugin.ets @@ -25,10 +25,13 @@ import BasicMessageChannel, { Reply } from '@ohos/flutter_ohos/src/main/ets/plug import { BinaryMessenger } from '@ohos/flutter_ohos/src/main/ets/plugin/common/BinaryMessenger'; import { SharedPreferencesListEncoder } from './SharedPreferencesListEncoder'; import buffer from '@ohos.buffer'; +import { LongStringStorage } from './LongStringStorage'; const TAG = "SharedPreferencesOhosPlugin" const PREFERENCES_NAME = "FlutterSharedPreferences"; const LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; +const MAX_VALUE_LENGTH = 8 * 1024; +const LONG_STRING_IDENTIFIER = 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxvbmcgc3RyaW5nLg=='; export default class SharedPreferencesOhosPlugin implements FlutterPlugin, SharedPreferencesApi { @@ -251,13 +254,15 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share remove(key: string): boolean { try { - this.preferences?.delete(key, (err: ESObject) => { - if (err) { - Log.w(TAG, "Failed to delete. message =" + err.message); - return false; - } - Log.i(TAG, "Succeeded in deleting."); - return true; + this.deleteLongStringIfExists(key).then(() => { + this.preferences?.delete(key, (err: ESObject) => { + if (err) { + Log.w(TAG, "Failed to delete. message =" + err.message); + return false; + } + Log.i(TAG, "Succeeded in deleting."); + return true; + }) }) } catch (err) { Log.e(TAG, "Failed to delete. " + JSON.stringify(err)); @@ -265,6 +270,23 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share return false; } + async deleteLongStringIfExists(key: string): Promise { + if (!LongStringStorage.inUse) { + return; + } + + let value = await this.preferences?.get(key, null); + if (typeof value == "string") { + let stringValue = value as string; + if (stringValue.startsWith(LONG_STRING_IDENTIFIER)) { + let filePath = `${LongStringStorage.rootDirectory}/${key}`; + return LongStringStorage.delete(filePath).then((_) => { + return Promise.resolve(); + }); + } + } + } + setString(key: string, value: string): Promise { if (value.startsWith(LIST_IDENTIFIER)) { throw new Error( @@ -280,13 +302,24 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share return this.put(key, value); } - put(key: string, value: ESObject): Promise { + async put(key: string, value: ESObject): Promise { try { if (this.preferences == null) { return new Promise((reject) => { reject(); }); } else { + if (typeof value == 'string') { + let byteLength = buffer.byteLength(value); + if (byteLength >= MAX_VALUE_LENGTH) { + let filePath = await LongStringStorage.put(key, value); + if (filePath != null) { + await this.preferences.put(key, LONG_STRING_IDENTIFIER + filePath); + return this.preferences?.flush(); + } + } + } + await this.deleteLongStringIfExists(key); this.preferences.put(key, value); return this.preferences.flush(); } @@ -310,10 +343,11 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share clear(prefix: string, allowList: string[]): Promise { try { - this.preferences?.getAll().then((value: object) => { + this.preferences?.getAll().then(async (value: object) => { let allKeys = Object.keys(value); for (let key of allKeys) { if (key.startsWith(prefix) && (allowList == null || allowList.indexOf(key) != -1)) { + await this.deleteLongStringIfExists(key); this.preferences?.delete(key); } } @@ -329,14 +363,16 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share }) } - filterData(value: [string, Object], prefix: string, allowList: string[]): Map { + async filterData(value: [string, Object], prefix: string, allowList: string[]): Promise> { let allVal = Object.entries(value); let filteredPrefs = new Map(); for (let val of allVal) { let key = val[0]; let v = val[1]; if (key.startsWith(prefix) && (allowList == null || allowList.indexOf(key) != -1)) { - filteredPrefs.set(key, this.transformPref(v)); + await this.transformPref(v).then((value) => { + filteredPrefs.set(key, value); + }); Log.w(TAG, "filterData00:key:" + key + " val:" + (this.transformPref(v))); } } @@ -352,8 +388,8 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share reject("Failed to getAll"); }) } - await this.preferences.getAll().then((obj: Object) => { - res = this.filterData(obj as [string, ESObject], prefix, allowList); + await this.preferences.getAll().then(async (obj: Object) => { + res = await this.filterData(obj as [string, ESObject], prefix, allowList); }) return new Promise((resolve) => { resolve(res); @@ -375,6 +411,7 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share let promise = data_preferences.getPreferences(binding.getApplicationContext(), PREFERENCES_NAME); promise.then((object) => { this.preferences = object; + LongStringStorage.init(binding.getApplicationContext()); Log.i(TAG, "Succeeded in getting preferences."); }).catch((err: ESObject) => { Log.w(TAG, "Failed to get preferences. code =" + err.code + ", message =" + err.message); @@ -403,13 +440,25 @@ export default class SharedPreferencesOhosPlugin implements FlutterPlugin, Share return this.put(key, value) } - transformPref(value: Object): Object { + async transformPref(value: Object): Promise { if (typeof value == "string") { let stringValue = (value as string); if (stringValue.startsWith(LIST_IDENTIFIER)) { let strList: ESObject = this.listEncoder.decode(stringValue.substring(LIST_IDENTIFIER.length)); let t: ESObject = JSON.parse(strList); return t; + } else if (stringValue.startsWith(LONG_STRING_IDENTIFIER)) { + let filePath = stringValue.substring(LONG_STRING_IDENTIFIER.length); + let longString: string | undefined = await LongStringStorage.get(filePath); + if (longString != undefined) { + if (longString.startsWith(LIST_IDENTIFIER)) { + let strList: ESObject = this.listEncoder.decode(longString.substring(LIST_IDENTIFIER.length)); + let t: ESObject = JSON.parse(strList); + return t; + } + return longString; + } + return Promise.reject(); } } return value;