[FL-3055] Getter for application data path (#2181)
* Threads: application id * Unit tests: appsdata getter test * Unit tests: moar test cases for appsdata getter * Unit tests: remove folders after test * Storage: dir_is_exist, migrate, + unit_tests * Plugins: migration * Storage: common_exists, moar unit_tests 4 "common_migrate", "common_migrate" and "common_merge" bugfixes * Storage: use FuriString for path handling * Storage API: send caller thread id with path * Storage: remove StorageType field in storage file list * Storage: simplify processing * Storage API: send caller thread id with path everywhere * Storage: /app alias, unit tests and path creation * Storage, path helper: remove unused * Examples: app data example * App plugins: use new VFS path * Storage: file_info_is_dir * Services: handle alias if the service accepts a path. * App plugins: fixes * Make PVS happy * Storage: fix storage_merge_recursive * Storage: rename process_aliases to resolve_path. Rename APPS_DATA to APP_DATA. * Apps: use predefined macro instead of raw paths. Example Apps Data: README fixes. * Storage: rename storage_common_resolve_path to storage_common_resolve_path_and_ensure_app_directory * Api: fix version * Storage: rename alias message * Storage: do not create app folders in path resolving process in certain cases. --------- Co-authored-by: Astra <93453568+Astrrra@users.noreply.github.com> Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									9ae58f5462
								
							
						
					
					
						commit
						777a4d109d
					
				| @ -44,3 +44,6 @@ | ||||
| 
 | ||||
| # Functions that always return the same error code | ||||
| //-V:picopass_device_decrypt:1048 | ||||
| 
 | ||||
| # Examples | ||||
| //V_EXCLUDE_PATH applications/examples/ | ||||
| @ -191,7 +191,7 @@ static void clean_directory(Storage* fs_api, const char* clean_dir) { | ||||
|             size_t size = strlen(clean_dir) + strlen(name) + 1 + 1; | ||||
|             char* fullname = malloc(size); | ||||
|             snprintf(fullname, size, "%s/%s", clean_dir, name); | ||||
|             if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|             if(file_info_is_dir(&fileinfo)) { | ||||
|                 clean_directory(fs_api, fullname); | ||||
|             } | ||||
|             FS_Error error = storage_common_remove(fs_api, fullname); | ||||
| @ -608,9 +608,8 @@ static void test_rpc_storage_list_create_expected_list( | ||||
|             } | ||||
| 
 | ||||
|             if(path_contains_only_ascii(name)) { | ||||
|                 list->file[i].type = (fileinfo.flags & FSF_DIRECTORY) ? | ||||
|                                          PB_Storage_File_FileType_DIR : | ||||
|                                          PB_Storage_File_FileType_FILE; | ||||
|                 list->file[i].type = file_info_is_dir(&fileinfo) ? PB_Storage_File_FileType_DIR : | ||||
|                                                                    PB_Storage_File_FileType_FILE; | ||||
|                 list->file[i].size = fileinfo.size; | ||||
|                 list->file[i].data = NULL; | ||||
|                 /* memory free inside rpc_encode_and_send() -> pb_release() */ | ||||
| @ -873,7 +872,7 @@ static void test_rpc_storage_stat_run(const char* path, uint32_t command_id) { | ||||
|     if(error == FSE_OK) { | ||||
|         response->which_content = PB_Main_storage_stat_response_tag; | ||||
|         response->content.storage_stat_response.has_file = true; | ||||
|         response->content.storage_stat_response.file.type = (fileinfo.flags & FSF_DIRECTORY) ? | ||||
|         response->content.storage_stat_response.file.type = file_info_is_dir(&fileinfo) ? | ||||
|                                                                 PB_Storage_File_FileType_DIR : | ||||
|                                                                 PB_Storage_File_FileType_FILE; | ||||
|         response->content.storage_stat_response.file.size = fileinfo.size; | ||||
|  | ||||
| @ -179,7 +179,7 @@ MU_TEST_1(test_dirwalk_full, Storage* storage) { | ||||
| 
 | ||||
|     while(dir_walk_read(dir_walk, path, &fileinfo) == DirWalkOK) { | ||||
|         furi_string_right(path, strlen(EXT_PATH("dirwalk/"))); | ||||
|         mu_check(storage_test_paths_mark(paths, path, (fileinfo.flags & FSF_DIRECTORY))); | ||||
|         mu_check(storage_test_paths_mark(paths, path, file_info_is_dir(&fileinfo))); | ||||
|     } | ||||
| 
 | ||||
|     dir_walk_free(dir_walk); | ||||
| @ -204,7 +204,7 @@ MU_TEST_1(test_dirwalk_no_recursive, Storage* storage) { | ||||
| 
 | ||||
|     while(dir_walk_read(dir_walk, path, &fileinfo) == DirWalkOK) { | ||||
|         furi_string_right(path, strlen(EXT_PATH("dirwalk/"))); | ||||
|         mu_check(storage_test_paths_mark(paths, path, (fileinfo.flags & FSF_DIRECTORY))); | ||||
|         mu_check(storage_test_paths_mark(paths, path, file_info_is_dir(&fileinfo))); | ||||
|     } | ||||
| 
 | ||||
|     dir_walk_free(dir_walk); | ||||
| @ -219,7 +219,7 @@ static bool test_dirwalk_filter_no_folder_ext(const char* name, FileInfo* filein | ||||
|     UNUSED(ctx); | ||||
| 
 | ||||
|     // only files
 | ||||
|     if(!(fileinfo->flags & FSF_DIRECTORY)) { | ||||
|     if(!file_info_is_dir(fileinfo)) { | ||||
|         // with ".test" in name
 | ||||
|         if(strstr(name, ".test") != NULL) { | ||||
|             return true; | ||||
| @ -243,7 +243,7 @@ MU_TEST_1(test_dirwalk_filter, Storage* storage) { | ||||
| 
 | ||||
|     while(dir_walk_read(dir_walk, path, &fileinfo) == DirWalkOK) { | ||||
|         furi_string_right(path, strlen(EXT_PATH("dirwalk/"))); | ||||
|         mu_check(storage_test_paths_mark(paths, path, (fileinfo.flags & FSF_DIRECTORY))); | ||||
|         mu_check(storage_test_paths_mark(paths, path, file_info_is_dir(&fileinfo))); | ||||
|     } | ||||
| 
 | ||||
|     dir_walk_free(dir_walk); | ||||
|  | ||||
| @ -2,9 +2,40 @@ | ||||
| #include <furi.h> | ||||
| #include <storage/storage.h> | ||||
| 
 | ||||
| // DO NOT USE THIS IN PRODUCTION CODE
 | ||||
| // This is a hack to access internal storage functions and definitions
 | ||||
| #include <storage/storage_i.h> | ||||
| 
 | ||||
| #define UNIT_TESTS_PATH(path) EXT_PATH("unit_tests/" path) | ||||
| 
 | ||||
| #define STORAGE_LOCKED_FILE EXT_PATH("locked_file.test") | ||||
| #define STORAGE_LOCKED_DIR STORAGE_INT_PATH_PREFIX | ||||
| 
 | ||||
| #define STORAGE_TEST_DIR UNIT_TESTS_PATH("test_dir") | ||||
| 
 | ||||
| static bool storage_file_create(Storage* storage, const char* path, const char* data) { | ||||
|     File* file = storage_file_alloc(storage); | ||||
|     bool result = false; | ||||
|     do { | ||||
|         if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_NEW)) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(storage_file_write(file, data, strlen(data)) != strlen(data)) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         if(!storage_file_close(file)) { | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         result = true; | ||||
|     } while(0); | ||||
| 
 | ||||
|     storage_file_free(file); | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| static void storage_file_open_lock_setup() { | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     File* file = storage_file_alloc(storage); | ||||
| @ -115,7 +146,7 @@ static int32_t storage_dir_locker(void* ctx) { | ||||
|     File* file = storage_file_alloc(storage); | ||||
|     furi_check(storage_dir_open(file, STORAGE_LOCKED_DIR)); | ||||
|     furi_semaphore_release(semaphore); | ||||
|     furi_delay_ms(1000); | ||||
|     furi_delay_ms(100); | ||||
| 
 | ||||
|     furi_check(storage_dir_close(file)); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| @ -152,9 +183,21 @@ MU_TEST(storage_dir_open_lock) { | ||||
|     mu_assert(result, "cannot open locked dir"); | ||||
| } | ||||
| 
 | ||||
| MU_TEST(storage_dir_exists_test) { | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
| 
 | ||||
|     mu_check(!storage_dir_exists(storage, STORAGE_TEST_DIR)); | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, STORAGE_TEST_DIR)); | ||||
|     mu_check(storage_dir_exists(storage, STORAGE_TEST_DIR)); | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_remove(storage, STORAGE_TEST_DIR)); | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| } | ||||
| 
 | ||||
| MU_TEST_SUITE(storage_dir) { | ||||
|     MU_RUN_TEST(storage_dir_open_close); | ||||
|     MU_RUN_TEST(storage_dir_open_lock); | ||||
|     MU_RUN_TEST(storage_dir_exists_test); | ||||
| } | ||||
| 
 | ||||
| static const char* const storage_copy_test_paths[] = { | ||||
| @ -303,9 +346,256 @@ MU_TEST_SUITE(storage_rename) { | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| } | ||||
| 
 | ||||
| #define APPSDATA_APP_PATH(path) APPS_DATA_PATH "/" path | ||||
| 
 | ||||
| static const char* storage_test_apps[] = { | ||||
|     "-_twilight_-", | ||||
|     "-_rainbow_-", | ||||
|     "-_pinkie_-", | ||||
|     "-_apple_-", | ||||
|     "-_flutter_-", | ||||
|     "-_rare_-", | ||||
| }; | ||||
| 
 | ||||
| static size_t storage_test_apps_count = COUNT_OF(storage_test_apps); | ||||
| 
 | ||||
| static int32_t storage_test_app(void* arg) { | ||||
|     UNUSED(arg); | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     storage_common_remove(storage, "/app/test"); | ||||
|     int32_t ret = storage_file_create(storage, "/app/test", "test"); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_storage_data_path_apps) { | ||||
|     for(size_t i = 0; i < storage_test_apps_count; i++) { | ||||
|         FuriThread* thread = | ||||
|             furi_thread_alloc_ex(storage_test_apps[i], 1024, storage_test_app, NULL); | ||||
|         furi_thread_set_appid(thread, storage_test_apps[i]); | ||||
|         furi_thread_start(thread); | ||||
|         furi_thread_join(thread); | ||||
| 
 | ||||
|         mu_assert_int_eq(true, furi_thread_get_return_code(thread)); | ||||
| 
 | ||||
|         // Check if app data dir and file exists
 | ||||
|         Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|         FuriString* expected = furi_string_alloc(); | ||||
|         furi_string_printf(expected, APPSDATA_APP_PATH("%s"), storage_test_apps[i]); | ||||
| 
 | ||||
|         mu_check(storage_dir_exists(storage, furi_string_get_cstr(expected))); | ||||
|         furi_string_cat(expected, "/test"); | ||||
|         mu_check(storage_file_exists(storage, furi_string_get_cstr(expected))); | ||||
| 
 | ||||
|         furi_string_printf(expected, APPSDATA_APP_PATH("%s"), storage_test_apps[i]); | ||||
|         storage_simply_remove_recursive(storage, furi_string_get_cstr(expected)); | ||||
| 
 | ||||
|         furi_record_close(RECORD_STORAGE); | ||||
| 
 | ||||
|         furi_string_free(expected); | ||||
|         furi_thread_free(thread); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_storage_data_path) { | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
| 
 | ||||
|     File* file = storage_file_alloc(storage); | ||||
|     mu_check(storage_dir_open(file, "/app")); | ||||
|     mu_check(storage_dir_close(file)); | ||||
|     storage_file_free(file); | ||||
| 
 | ||||
|     // check that appsdata folder exists
 | ||||
|     mu_check(storage_dir_exists(storage, APPS_DATA_PATH)); | ||||
| 
 | ||||
|     // check that cli folder exists
 | ||||
|     mu_check(storage_dir_exists(storage, APPSDATA_APP_PATH("cli"))); | ||||
| 
 | ||||
|     storage_simply_remove(storage, APPSDATA_APP_PATH("cli")); | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| } | ||||
| 
 | ||||
| MU_TEST(test_storage_common_migrate) { | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
| 
 | ||||
|     // Setup test folders
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
| 
 | ||||
|     // Test migration from non existing
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     // Test migration from existing folder to non existing
 | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old/file1"), "test1")); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old/file2.ext"), "test2")); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old/file3.ext.ext"), "test3")); | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file1"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file2.ext"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file3.ext.ext"))); | ||||
|     mu_check(storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
| 
 | ||||
|     // Test migration from existing folder to existing folder
 | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old/file1"), "test1")); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old/file2.ext"), "test2")); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old/file3.ext.ext"), "test3")); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file1"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file2.ext"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file3.ext.ext"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file11"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file21.ext"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new/file3.ext1.ext"))); | ||||
|     mu_check(storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
| 
 | ||||
|     // Test migration from empty folder to existing file
 | ||||
|     // Expected result: FSE_OK, folder removed, file untouched
 | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_new"), "test1")); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
| 
 | ||||
|     // Test migration from empty folder to existing folder
 | ||||
|     // Expected result: FSE_OK, old folder removed, new folder untouched
 | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
| 
 | ||||
|     // Test migration from existing file to non existing, no extension
 | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old"), "test1")); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_file_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
| 
 | ||||
|     // Test migration from existing file to non existing, with extension
 | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old.file"), "test1")); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old.file"), UNIT_TESTS_PATH("migrate_new.file"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new.file"))); | ||||
|     mu_check(!storage_file_exists(storage, UNIT_TESTS_PATH("migrate_old.file"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old.file")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new.file")); | ||||
| 
 | ||||
|     // Test migration from existing file to existing file, no extension
 | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old"), "test1")); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_new"), "test2")); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_file_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new1"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new1")); | ||||
| 
 | ||||
|     // Test migration from existing file to existing file, with extension
 | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old.file"), "test1")); | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_new.file"), "test2")); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old.file"), UNIT_TESTS_PATH("migrate_new.file"))); | ||||
| 
 | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new.file"))); | ||||
|     mu_check(!storage_file_exists(storage, UNIT_TESTS_PATH("migrate_old.file"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new1.file"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old.file")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new.file")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new1.file")); | ||||
| 
 | ||||
|     // Test migration from existing file to existing folder
 | ||||
|     mu_check(storage_file_create(storage, UNIT_TESTS_PATH("migrate_old"), "test1")); | ||||
|     mu_assert_int_eq(FSE_OK, storage_common_mkdir(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_assert_int_eq( | ||||
|         FSE_OK, | ||||
|         storage_common_migrate( | ||||
|             storage, UNIT_TESTS_PATH("migrate_old"), UNIT_TESTS_PATH("migrate_new"))); | ||||
| 
 | ||||
|     mu_check(storage_dir_exists(storage, UNIT_TESTS_PATH("migrate_new"))); | ||||
|     mu_check(!storage_file_exists(storage, UNIT_TESTS_PATH("migrate_old"))); | ||||
|     mu_check(storage_file_exists(storage, UNIT_TESTS_PATH("migrate_new1"))); | ||||
| 
 | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_old")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new")); | ||||
|     storage_simply_remove_recursive(storage, UNIT_TESTS_PATH("migrate_new1")); | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| } | ||||
| 
 | ||||
| MU_TEST_SUITE(test_data_path) { | ||||
|     MU_RUN_TEST(test_storage_data_path); | ||||
|     MU_RUN_TEST(test_storage_data_path_apps); | ||||
| } | ||||
| 
 | ||||
| MU_TEST_SUITE(test_storage_common) { | ||||
|     MU_RUN_TEST(test_storage_common_migrate); | ||||
| } | ||||
| 
 | ||||
| int run_minunit_test_storage() { | ||||
|     MU_RUN_SUITE(storage_file); | ||||
|     MU_RUN_SUITE(storage_dir); | ||||
|     MU_RUN_SUITE(storage_rename); | ||||
|     MU_RUN_SUITE(test_data_path); | ||||
|     MU_RUN_SUITE(test_storage_common); | ||||
|     return MU_EXIT_CODE; | ||||
| } | ||||
|  | ||||
							
								
								
									
										18
									
								
								applications/examples/example_apps_data/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								applications/examples/example_apps_data/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| # Apps Data folder Example | ||||
| 
 | ||||
| This example demonstrates how to utilize the Apps Data folder to store data that is not part of the app itself, such as user data, configuration files, and so forth. | ||||
| 
 | ||||
| ## What is the Apps Data Folder? | ||||
| 
 | ||||
| The **Apps Data** folder is a folder used to store data for external apps that are not part of the main firmware.  | ||||
| 
 | ||||
| The path to the current application folder is related to the `appid` of the app. The `appid` is used to identify the app in the app store and is stored in the `application.fam` file.  | ||||
| The Apps Data folder is located only on the external storage, the SD card. | ||||
| 
 | ||||
| For example, if the `appid` of the app is `snake_game`, the path to the Apps Data folder will be `/ext/apps_data/snake_game`. But using raw paths is not recommended, because the path to the Apps Data folder can change in the future. Use the `/app` alias instead. | ||||
| 
 | ||||
| ## How to get the path to the Apps Data folder? | ||||
| 
 | ||||
| You can use `/app` alias to get the path to the current application data folder. For example, if you want to open a file `config.txt` in the Apps Data folder, you can use the next path: `/app/config.txt`. But this way is not recommended, because even the `/app` alias can change in the future. | ||||
| 
 | ||||
| We recommend to use the `APP_DATA_PATH` macro to get the path to the Apps Data folder. For example, if you want to open a file `config.txt` in the Apps Data folder, you can use the next path: `APP_DATA_PATH("config.txt")`. | ||||
							
								
								
									
										9
									
								
								applications/examples/example_apps_data/application.fam
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								applications/examples/example_apps_data/application.fam
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| App( | ||||
|     appid="example_apps_data", | ||||
|     name="Example: Apps Data", | ||||
|     apptype=FlipperAppType.EXTERNAL, | ||||
|     entry_point="example_apps_data_main", | ||||
|     requires=["gui"], | ||||
|     stack_size=1 * 1024, | ||||
|     fap_category="Examples", | ||||
| ) | ||||
							
								
								
									
										40
									
								
								applications/examples/example_apps_data/example_apps_data.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								applications/examples/example_apps_data/example_apps_data.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| #include <furi.h> | ||||
| #include <storage/storage.h> | ||||
| 
 | ||||
| // Define log tag
 | ||||
| #define TAG "example_apps_data" | ||||
| 
 | ||||
| // Application entry point
 | ||||
| int32_t example_apps_data_main(void* p) { | ||||
|     // Mark argument as unused
 | ||||
|     UNUSED(p); | ||||
| 
 | ||||
|     // Open storage
 | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
| 
 | ||||
|     // Allocate file
 | ||||
|     File* file = storage_file_alloc(storage); | ||||
| 
 | ||||
|     // Get the path to the current application data folder
 | ||||
|     // That is: /ext/apps_data/<app_name>
 | ||||
|     // And it will create folders in the path if they don't exist
 | ||||
|     // In this example it will create /ext/apps_data/example_apps_data
 | ||||
|     // And file will be /ext/apps_data/example_apps_data/test.txt
 | ||||
| 
 | ||||
|     // Open file, write data and close it
 | ||||
|     if(!storage_file_open(file, APP_DATA_PATH("test.txt"), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { | ||||
|         FURI_LOG_E(TAG, "Failed to open file"); | ||||
|     } | ||||
|     if(!storage_file_write(file, "Hello World!", strlen("Hello World!"))) { | ||||
|         FURI_LOG_E(TAG, "Failed to write to file"); | ||||
|     } | ||||
|     storage_file_close(file); | ||||
| 
 | ||||
|     // Deallocate file
 | ||||
|     storage_file_free(file); | ||||
| 
 | ||||
|     // Close storage
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| @ -436,7 +436,7 @@ static bool archive_is_dir_exists(FuriString* path) { | ||||
|     FileInfo file_info; | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     if(storage_common_stat(storage, furi_string_get_cstr(path), &file_info) == FSE_OK) { | ||||
|         if(file_info.flags & FSF_DIRECTORY) { | ||||
|         if(file_info_is_dir(&file_info)) { | ||||
|             state = true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -160,7 +160,7 @@ bool archive_favorites_read(void* context) { | ||||
|                 if(storage_file_exists(storage, furi_string_get_cstr(buffer))) { | ||||
|                     storage_common_stat(storage, furi_string_get_cstr(buffer), &file_info); | ||||
|                     archive_add_file_item( | ||||
|                         browser, (file_info.flags & FSF_DIRECTORY), furi_string_get_cstr(buffer)); | ||||
|                         browser, file_info_is_dir(&file_info), furi_string_get_cstr(buffer)); | ||||
|                     file_count++; | ||||
|                 } else { | ||||
|                     need_refresh = true; | ||||
|  | ||||
| @ -91,7 +91,7 @@ void archive_delete_file(void* context, const char* format, ...) { | ||||
| 
 | ||||
|     bool res = false; | ||||
| 
 | ||||
|     if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|     if(file_info_is_dir(&fileinfo)) { | ||||
|         res = storage_simply_remove_recursive(fs_api, furi_string_get_cstr(filename)); | ||||
|     } else { | ||||
|         res = (storage_common_remove(fs_api, furi_string_get_cstr(filename)) == FSE_OK); | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| #include <storage/storage.h> | ||||
| #include <gui/modules/loading.h> | ||||
| #include <dialogs/dialogs.h> | ||||
| #include <toolbox/path.h> | ||||
| #include <flipper_application/flipper_application.h> | ||||
| #include "elf_cpp/elf_hashtable.h" | ||||
| #include "fap_loader_app.h" | ||||
| @ -105,6 +106,12 @@ static bool fap_loader_run_selected_app(FapLoader* loader) { | ||||
|         FURI_LOG_I(TAG, "FAP Loader is starting app"); | ||||
| 
 | ||||
|         FuriThread* thread = flipper_application_spawn(loader->app, NULL); | ||||
| 
 | ||||
|         FuriString* app_name = furi_string_alloc(); | ||||
|         path_extract_filename_no_ext(furi_string_get_cstr(loader->fap_path), app_name); | ||||
|         furi_thread_set_appid(thread, furi_string_get_cstr(app_name)); | ||||
|         furi_string_free(app_name); | ||||
| 
 | ||||
|         furi_thread_start(thread); | ||||
|         furi_thread_join(thread); | ||||
| 
 | ||||
|  | ||||
| @ -376,7 +376,17 @@ int32_t hid_ble_app(void* p) { | ||||
|     // Wait 2nd core to update nvm storage
 | ||||
|     furi_delay_ms(200); | ||||
| 
 | ||||
|     bt_keys_storage_set_storage_path(app->bt, HID_BT_KEYS_STORAGE_PATH); | ||||
|     // Migrate data from old sd-card folder
 | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
| 
 | ||||
|     storage_common_migrate( | ||||
|         storage, | ||||
|         EXT_PATH("apps/Tools/" HID_BT_KEYS_STORAGE_NAME), | ||||
|         APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME)); | ||||
| 
 | ||||
|     bt_keys_storage_set_storage_path(app->bt, APP_DATA_PATH(HID_BT_KEYS_STORAGE_NAME)); | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| 
 | ||||
|     if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) { | ||||
|         FURI_LOG_E(TAG, "Failed to switch to HID profile"); | ||||
|  | ||||
| @ -23,7 +23,7 @@ | ||||
| #include "views/hid_mouse_jiggler.h" | ||||
| #include "views/hid_tiktok.h" | ||||
| 
 | ||||
| #define HID_BT_KEYS_STORAGE_PATH EXT_PATH("apps/Tools/.bt_hid.keys") | ||||
| #define HID_BT_KEYS_STORAGE_NAME ".bt_hid.keys" | ||||
| 
 | ||||
| typedef enum { | ||||
|     HidTransportUsb, | ||||
|  | ||||
| @ -10,7 +10,6 @@ | ||||
| 
 | ||||
| #define TAG "MusicPlayer" | ||||
| 
 | ||||
| #define MUSIC_PLAYER_APP_PATH_FOLDER ANY_PATH("music_player") | ||||
| #define MUSIC_PLAYER_APP_EXTENSION "*" | ||||
| 
 | ||||
| #define MUSIC_PLAYER_SEMITONE_HISTORY_SIZE 4 | ||||
| @ -307,18 +306,24 @@ int32_t music_player_app(void* p) { | ||||
|         if(p && strlen(p)) { | ||||
|             furi_string_set(file_path, (const char*)p); | ||||
|         } else { | ||||
|             furi_string_set(file_path, MUSIC_PLAYER_APP_PATH_FOLDER); | ||||
|             Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|             storage_common_migrate( | ||||
|                 storage, EXT_PATH("music_player"), STORAGE_APP_DATA_PATH_PREFIX); | ||||
|             furi_record_close(RECORD_STORAGE); | ||||
| 
 | ||||
|             furi_string_set(file_path, STORAGE_APP_DATA_PATH_PREFIX); | ||||
| 
 | ||||
|             DialogsFileBrowserOptions browser_options; | ||||
|             dialog_file_browser_set_basic_options( | ||||
|                 &browser_options, MUSIC_PLAYER_APP_EXTENSION, &I_music_10px); | ||||
|             browser_options.hide_ext = false; | ||||
|             browser_options.base_path = MUSIC_PLAYER_APP_PATH_FOLDER; | ||||
|             browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX; | ||||
| 
 | ||||
|             DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); | ||||
|             bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options); | ||||
| 
 | ||||
|             furi_record_close(RECORD_DIALOGS); | ||||
| 
 | ||||
|             if(!res) { | ||||
|                 FURI_LOG_E(TAG, "No file selected"); | ||||
|                 break; | ||||
|  | ||||
| @ -3,8 +3,8 @@ | ||||
| #include <lib/toolbox/args.h> | ||||
| #include <lib/flipper_format/flipper_format.h> | ||||
| 
 | ||||
| #define ICLASS_ELITE_DICT_FLIPPER_PATH EXT_PATH("picopass/assets/iclass_elite_dict.txt") | ||||
| #define ICLASS_ELITE_DICT_USER_PATH EXT_PATH("picopass/assets/iclass_elite_dict_user.txt") | ||||
| #define ICLASS_ELITE_DICT_FLIPPER_NAME APP_DATA_PATH("assets/iclass_elite_dict.txt") | ||||
| #define ICLASS_ELITE_DICT_USER_NAME APP_DATA_PATH("assets/iclass_elite_dict_user.txt") | ||||
| 
 | ||||
| #define TAG "IclassEliteDict" | ||||
| 
 | ||||
| @ -21,10 +21,10 @@ bool iclass_elite_dict_check_presence(IclassEliteDictType dict_type) { | ||||
| 
 | ||||
|     bool dict_present = false; | ||||
|     if(dict_type == IclassEliteDictTypeFlipper) { | ||||
|         dict_present = storage_common_stat(storage, ICLASS_ELITE_DICT_FLIPPER_PATH, NULL) == | ||||
|                        FSE_OK; | ||||
|         dict_present = | ||||
|             (storage_common_stat(storage, ICLASS_ELITE_DICT_FLIPPER_NAME, NULL) == FSE_OK); | ||||
|     } else if(dict_type == IclassEliteDictTypeUser) { | ||||
|         dict_present = storage_common_stat(storage, ICLASS_ELITE_DICT_USER_PATH, NULL) == FSE_OK; | ||||
|         dict_present = (storage_common_stat(storage, ICLASS_ELITE_DICT_USER_NAME, NULL) == FSE_OK); | ||||
|     } | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| @ -36,27 +36,26 @@ IclassEliteDict* iclass_elite_dict_alloc(IclassEliteDictType dict_type) { | ||||
|     IclassEliteDict* dict = malloc(sizeof(IclassEliteDict)); | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     dict->stream = buffered_file_stream_alloc(storage); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|     FuriString* next_line = furi_string_alloc(); | ||||
| 
 | ||||
|     bool dict_loaded = false; | ||||
|     do { | ||||
|         if(dict_type == IclassEliteDictTypeFlipper) { | ||||
|             if(!buffered_file_stream_open( | ||||
|                    dict->stream, ICLASS_ELITE_DICT_FLIPPER_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) { | ||||
|                    dict->stream, ICLASS_ELITE_DICT_FLIPPER_NAME, FSAM_READ, FSOM_OPEN_EXISTING)) { | ||||
|                 buffered_file_stream_close(dict->stream); | ||||
|                 break; | ||||
|             } | ||||
|         } else if(dict_type == IclassEliteDictTypeUser) { | ||||
|             if(!buffered_file_stream_open( | ||||
|                    dict->stream, ICLASS_ELITE_DICT_USER_PATH, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS)) { | ||||
|                    dict->stream, ICLASS_ELITE_DICT_USER_NAME, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS)) { | ||||
|                 buffered_file_stream_close(dict->stream); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Read total amount of keys
 | ||||
|         while(true) { | ||||
|         while(true) { //-V547
 | ||||
|             if(!stream_read_line(dict->stream, next_line)) break; | ||||
|             if(furi_string_get_char(next_line, 0) == '#') continue; | ||||
|             if(furi_string_size(next_line) != ICLASS_ELITE_KEY_LINE_LEN) continue; | ||||
| @ -69,12 +68,13 @@ IclassEliteDict* iclass_elite_dict_alloc(IclassEliteDictType dict_type) { | ||||
|         FURI_LOG_I(TAG, "Loaded dictionary with %lu keys", dict->total_keys); | ||||
|     } while(false); | ||||
| 
 | ||||
|     if(!dict_loaded) { | ||||
|     if(!dict_loaded) { //-V547
 | ||||
|         buffered_file_stream_close(dict->stream); | ||||
|         free(dict); | ||||
|         dict = NULL; | ||||
|     } | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|     furi_string_free(next_line); | ||||
| 
 | ||||
|     return dict; | ||||
|  | ||||
| @ -171,6 +171,12 @@ void picopass_show_loading_popup(void* context, bool show) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static void picopass_migrate_from_old_folder() { | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     storage_common_migrate(storage, "/ext/picopass", STORAGE_APP_DATA_PATH_PREFIX); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| } | ||||
| 
 | ||||
| bool picopass_is_memset(const uint8_t* data, const uint8_t pattern, size_t size) { | ||||
|     bool result = size > 0; | ||||
|     while(size > 0) { | ||||
| @ -183,6 +189,8 @@ bool picopass_is_memset(const uint8_t* data, const uint8_t pattern, size_t size) | ||||
| 
 | ||||
| int32_t picopass_app(void* p) { | ||||
|     UNUSED(p); | ||||
|     picopass_migrate_from_old_folder(); | ||||
| 
 | ||||
|     Picopass* picopass = picopass_alloc(); | ||||
| 
 | ||||
|     scene_manager_next_scene(picopass->scene_manager, PicopassSceneStart); | ||||
|  | ||||
| @ -48,13 +48,9 @@ static bool picopass_device_save_file( | ||||
|         if(use_load_path && !furi_string_empty(dev->load_path)) { | ||||
|             // Get directory name
 | ||||
|             path_extract_dirname(furi_string_get_cstr(dev->load_path), temp_str); | ||||
|             // Create picopass directory if necessary
 | ||||
|             if(!storage_simply_mkdir(dev->storage, furi_string_get_cstr(temp_str))) break; | ||||
|             // Make path to file to save
 | ||||
|             furi_string_cat_printf(temp_str, "/%s%s", dev_name, extension); | ||||
|         } else { | ||||
|             // Create picopass directory if necessary
 | ||||
|             if(!storage_simply_mkdir(dev->storage, PICOPASS_APP_FOLDER)) break; | ||||
|             // First remove picopass device file if it was saved
 | ||||
|             furi_string_printf(temp_str, "%s/%s%s", folder, dev_name, extension); | ||||
|         } | ||||
| @ -126,10 +122,11 @@ static bool picopass_device_save_file( | ||||
| bool picopass_device_save(PicopassDevice* dev, const char* dev_name) { | ||||
|     if(dev->format == PicopassDeviceSaveFormatHF) { | ||||
|         return picopass_device_save_file( | ||||
|             dev, dev_name, PICOPASS_APP_FOLDER, PICOPASS_APP_EXTENSION, true); | ||||
|             dev, dev_name, STORAGE_APP_DATA_PATH_PREFIX, PICOPASS_APP_EXTENSION, true); | ||||
|     } else if(dev->format == PicopassDeviceSaveFormatLF) { | ||||
|         return picopass_device_save_file(dev, dev_name, ANY_PATH("lfrfid"), ".rfid", true); | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| @ -225,13 +222,12 @@ void picopass_device_free(PicopassDevice* picopass_dev) { | ||||
| bool picopass_file_select(PicopassDevice* dev) { | ||||
|     furi_assert(dev); | ||||
| 
 | ||||
|     // Input events and views are managed by file_browser
 | ||||
|     FuriString* picopass_app_folder; | ||||
|     picopass_app_folder = furi_string_alloc_set(PICOPASS_APP_FOLDER); | ||||
|     picopass_app_folder = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX); | ||||
| 
 | ||||
|     DialogsFileBrowserOptions browser_options; | ||||
|     dialog_file_browser_set_basic_options(&browser_options, PICOPASS_APP_EXTENSION, &I_Nfc_10px); | ||||
|     browser_options.base_path = PICOPASS_APP_FOLDER; | ||||
|     browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX; | ||||
| 
 | ||||
|     bool res = dialog_file_browser_show( | ||||
|         dev->dialogs, dev->load_path, picopass_app_folder, &browser_options); | ||||
| @ -274,7 +270,7 @@ bool picopass_device_delete(PicopassDevice* dev, bool use_load_path) { | ||||
|             furi_string_set(file_path, dev->load_path); | ||||
|         } else { | ||||
|             furi_string_printf( | ||||
|                 file_path, "%s/%s%s", PICOPASS_APP_FOLDER, dev->dev_name, PICOPASS_APP_EXTENSION); | ||||
|                 file_path, APP_DATA_PATH("%s%s"), dev->dev_name, PICOPASS_APP_EXTENSION); | ||||
|         } | ||||
|         if(!storage_simply_remove(dev->storage, furi_string_get_cstr(file_path))) break; | ||||
|         deleted = true; | ||||
|  | ||||
| @ -24,7 +24,6 @@ | ||||
| #define PICOPASS_AIA_BLOCK_INDEX 5 | ||||
| #define PICOPASS_PACS_CFG_BLOCK_INDEX 6 | ||||
| 
 | ||||
| #define PICOPASS_APP_FOLDER ANY_PATH("picopass") | ||||
| #define PICOPASS_APP_EXTENSION ".picopass" | ||||
| #define PICOPASS_APP_SHADOW_EXTENSION ".pas" | ||||
| 
 | ||||
| @ -81,7 +80,6 @@ typedef struct { | ||||
|     PicopassDeviceSaveFormat format; | ||||
|     PicopassLoadingCallback loading_cb; | ||||
|     void* loading_cb_ctx; | ||||
| 
 | ||||
| } PicopassDevice; | ||||
| 
 | ||||
| PicopassDevice* picopass_device_alloc(); | ||||
|  | ||||
| @ -31,12 +31,10 @@ void picopass_scene_save_name_on_enter(void* context) { | ||||
|         dev_name_empty); | ||||
| 
 | ||||
|     FuriString* folder_path; | ||||
|     folder_path = furi_string_alloc(); | ||||
|     folder_path = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX); | ||||
| 
 | ||||
|     if(furi_string_end_with(picopass->dev->load_path, PICOPASS_APP_EXTENSION)) { | ||||
|         path_extract_dirname(furi_string_get_cstr(picopass->dev->load_path), folder_path); | ||||
|     } else { | ||||
|         furi_string_set(folder_path, PICOPASS_APP_FOLDER); | ||||
|     } | ||||
| 
 | ||||
|     ValidatorIsFile* validator_is_file = validator_is_file_alloc_init( | ||||
|  | ||||
| @ -60,7 +60,7 @@ bool spi_mem_scene_start_on_event(void* context, SceneManagerEvent event) { | ||||
|             scene_manager_next_scene(app->scene_manager, SPIMemSceneChipDetect); | ||||
|             success = true; | ||||
|         } else if(event.event == SPIMemSceneStartSubmenuIndexSaved) { | ||||
|             furi_string_set(app->file_path, SPI_MEM_FILE_FOLDER); | ||||
|             furi_string_set(app->file_path, STORAGE_APP_DATA_PATH_PREFIX); | ||||
|             scene_manager_next_scene(app->scene_manager, SPIMemSceneSelectFile); | ||||
|             success = true; | ||||
|         } else if(event.event == SPIMemSceneStartSubmenuIndexErase) { | ||||
|  | ||||
| @ -16,9 +16,9 @@ static bool spi_mem_back_event_callback(void* context) { | ||||
| } | ||||
| 
 | ||||
| SPIMemApp* spi_mem_alloc(void) { | ||||
|     SPIMemApp* instance = malloc(sizeof(SPIMemApp)); | ||||
|     SPIMemApp* instance = malloc(sizeof(SPIMemApp)); //-V799
 | ||||
| 
 | ||||
|     instance->file_path = furi_string_alloc(); | ||||
|     instance->file_path = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX); | ||||
|     instance->gui = furi_record_open(RECORD_GUI); | ||||
|     instance->notifications = furi_record_open(RECORD_NOTIFICATION); | ||||
|     instance->view_dispatcher = view_dispatcher_alloc(); | ||||
| @ -37,7 +37,8 @@ SPIMemApp* spi_mem_alloc(void) { | ||||
|     instance->text_input = text_input_alloc(); | ||||
|     instance->mode = SPIMemModeUnknown; | ||||
| 
 | ||||
|     furi_string_set(instance->file_path, SPI_MEM_FILE_FOLDER); | ||||
|     // Migrate data from old sd-card folder
 | ||||
|     storage_common_migrate(instance->storage, EXT_PATH("spimem"), STORAGE_APP_DATA_PATH_PREFIX); | ||||
| 
 | ||||
|     view_dispatcher_enable_queue(instance->view_dispatcher); | ||||
|     view_dispatcher_set_event_callback_context(instance->view_dispatcher, instance); | ||||
| @ -70,7 +71,7 @@ SPIMemApp* spi_mem_alloc(void) { | ||||
|     furi_hal_spi_bus_handle_init(&furi_hal_spi_bus_handle_external); | ||||
|     scene_manager_next_scene(instance->scene_manager, SPIMemSceneStart); | ||||
|     return instance; | ||||
| } | ||||
| } //-V773
 | ||||
| 
 | ||||
| void spi_mem_free(SPIMemApp* instance) { | ||||
|     view_dispatcher_remove_view(instance->view_dispatcher, SPIMemViewSubmenu); | ||||
| @ -105,7 +106,6 @@ void spi_mem_free(SPIMemApp* instance) { | ||||
| int32_t spi_mem_app(void* p) { | ||||
|     UNUSED(p); | ||||
|     SPIMemApp* instance = spi_mem_alloc(); | ||||
|     spi_mem_file_create_folder(instance); | ||||
|     view_dispatcher_run(instance->view_dispatcher); | ||||
|     spi_mem_free(instance); | ||||
|     return 0; | ||||
|  | ||||
| @ -24,7 +24,6 @@ | ||||
| 
 | ||||
| #define TAG "SPIMem" | ||||
| #define SPI_MEM_FILE_EXTENSION ".bin" | ||||
| #define SPI_MEM_FILE_FOLDER EXT_PATH("spimem") | ||||
| #define SPI_MEM_FILE_NAME_SIZE 100 | ||||
| #define SPI_MEM_TEXT_BUFFER_SIZE 128 | ||||
| 
 | ||||
|  | ||||
| @ -1,11 +1,5 @@ | ||||
| #include "spi_mem_app_i.h" | ||||
| 
 | ||||
| void spi_mem_file_create_folder(SPIMemApp* app) { | ||||
|     if(!storage_simply_mkdir(app->storage, SPI_MEM_FILE_FOLDER)) { | ||||
|         dialog_message_show_storage_error(app->dialogs, "Cannot create\napp folder"); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| bool spi_mem_file_delete(SPIMemApp* app) { | ||||
|     return (storage_simply_remove(app->storage, furi_string_get_cstr(app->file_path))); | ||||
| } | ||||
| @ -13,7 +7,7 @@ bool spi_mem_file_delete(SPIMemApp* app) { | ||||
| bool spi_mem_file_select(SPIMemApp* app) { | ||||
|     DialogsFileBrowserOptions browser_options; | ||||
|     dialog_file_browser_set_basic_options(&browser_options, SPI_MEM_FILE_EXTENSION, &I_Dip8_10px); | ||||
|     browser_options.base_path = SPI_MEM_FILE_FOLDER; | ||||
|     browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX; | ||||
|     bool success = | ||||
|         dialog_file_browser_show(app->dialogs, app->file_path, app->file_path, &browser_options); | ||||
|     return success; | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| #pragma once | ||||
| #include "spi_mem_app.h" | ||||
| 
 | ||||
| void spi_mem_file_create_folder(SPIMemApp* app); | ||||
| bool spi_mem_file_select(SPIMemApp* app); | ||||
| bool spi_mem_file_create(SPIMemApp* app, const char* file_name); | ||||
| bool spi_mem_file_delete(SPIMemApp* app); | ||||
|  | ||||
| @ -11,6 +11,7 @@ typedef enum { | ||||
| typedef struct { | ||||
|     const FuriThreadCallback app; | ||||
|     const char* name; | ||||
|     const char* appid; | ||||
|     const size_t stack_size; | ||||
|     const Icon* icon; | ||||
|     const FlipperApplicationFlag flags; | ||||
|  | ||||
| @ -45,7 +45,14 @@ void bt_keys_storage_set_storage_path(Bt* bt, const char* keys_storage_path) { | ||||
|     furi_assert(bt->keys_storage); | ||||
|     furi_assert(keys_storage_path); | ||||
| 
 | ||||
|     bt_keys_storage_set_file_path(bt->keys_storage, keys_storage_path); | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     FuriString* path = furi_string_alloc_set(keys_storage_path); | ||||
|     storage_common_resolve_path_and_ensure_app_directory(storage, path); | ||||
| 
 | ||||
|     bt_keys_storage_set_file_path(bt->keys_storage, furi_string_get_cstr(path)); | ||||
| 
 | ||||
|     furi_string_free(path); | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
| } | ||||
| 
 | ||||
| void bt_keys_storage_set_default_path(Bt* bt) { | ||||
|  | ||||
| @ -372,11 +372,18 @@ void cli_command_ps(Cli* cli, FuriString* args, void* context) { | ||||
|     FuriThreadId threads_ids[threads_num_max]; | ||||
|     uint8_t thread_num = furi_thread_enumerate(threads_ids, threads_num_max); | ||||
|     printf( | ||||
|         "%-20s %-14s %-8s %-8s %s\r\n", "Name", "Stack start", "Heap", "Stack", "Stack min free"); | ||||
|         "%-20s %-20s %-14s %-8s %-8s %s\r\n", | ||||
|         "AppID", | ||||
|         "Name", | ||||
|         "Stack start", | ||||
|         "Heap", | ||||
|         "Stack", | ||||
|         "Stack min free"); | ||||
|     for(uint8_t i = 0; i < thread_num; i++) { | ||||
|         TaskControlBlock* tcb = (TaskControlBlock*)threads_ids[i]; | ||||
|         printf( | ||||
|             "%-20s 0x%-12lx %-8zu %-8lu %-8lu\r\n", | ||||
|             "%-20s %-20s 0x%-12lx %-8zu %-8lu %-8lu\r\n", | ||||
|             furi_thread_get_appid(threads_ids[i]), | ||||
|             furi_thread_get_name(threads_ids[i]), | ||||
|             (uint32_t)tcb->pxStack, | ||||
|             memmgr_heap_get_thread_memory(threads_ids[i]), | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| #include "dialogs_i.h" | ||||
| #include <toolbox/api_lock.h> | ||||
| #include <assets_icons.h> | ||||
| #include <storage/storage.h> | ||||
| 
 | ||||
| /****************** File browser ******************/ | ||||
| 
 | ||||
| @ -13,6 +14,22 @@ bool dialog_file_browser_show( | ||||
|     FuriApiLock lock = api_lock_alloc_locked(); | ||||
|     furi_check(lock != NULL); | ||||
| 
 | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     FuriString* base_path = furi_string_alloc(); | ||||
| 
 | ||||
|     if(options && options->base_path) { | ||||
|         furi_string_set(base_path, options->base_path); | ||||
|         storage_common_resolve_path_and_ensure_app_directory(storage, base_path); | ||||
|     } | ||||
| 
 | ||||
|     if(result_path) { | ||||
|         storage_common_resolve_path_and_ensure_app_directory(storage, result_path); | ||||
|     } | ||||
| 
 | ||||
|     if(path) { | ||||
|         storage_common_resolve_path_and_ensure_app_directory(storage, path); | ||||
|     } | ||||
| 
 | ||||
|     DialogsAppData data = { | ||||
|         .file_browser = { | ||||
|             .extension = options ? options->extension : "", | ||||
| @ -24,7 +41,7 @@ bool dialog_file_browser_show( | ||||
|             .preselected_filename = path, | ||||
|             .item_callback = options ? options->item_loader_callback : NULL, | ||||
|             .item_callback_context = options ? options->item_loader_context : NULL, | ||||
|             .base_path = options ? options->base_path : NULL, | ||||
|             .base_path = furi_string_get_cstr(base_path), | ||||
|         }}; | ||||
| 
 | ||||
|     DialogsAppReturn return_data; | ||||
| @ -39,6 +56,9 @@ bool dialog_file_browser_show( | ||||
|         furi_message_queue_put(context->message_queue, &message, FuriWaitForever) == FuriStatusOk); | ||||
|     api_lock_wait_unlock_and_free(lock); | ||||
| 
 | ||||
|     furi_record_close(RECORD_STORAGE); | ||||
|     furi_string_free(base_path); | ||||
| 
 | ||||
|     return return_data.bool_value; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -60,7 +60,7 @@ static bool browser_path_is_file(FuriString* path) { | ||||
|     FileInfo file_info; | ||||
|     Storage* storage = furi_record_open(RECORD_STORAGE); | ||||
|     if(storage_common_stat(storage, furi_string_get_cstr(path), &file_info) == FSE_OK) { | ||||
|         if((file_info.flags & FSF_DIRECTORY) == 0) { | ||||
|         if(!file_info_is_dir(&file_info)) { | ||||
|             state = true; | ||||
|         } | ||||
|     } | ||||
| @ -119,7 +119,7 @@ static bool browser_folder_check_and_switch(FuriString* path) { | ||||
|     while(1) { | ||||
|         // Check if folder is existing and navigate back if not
 | ||||
|         if(storage_common_stat(storage, furi_string_get_cstr(path), &file_info) == FSE_OK) { | ||||
|             if(file_info.flags & FSF_DIRECTORY) { | ||||
|             if(file_info_is_dir(&file_info)) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| @ -161,7 +161,7 @@ static bool browser_folder_init( | ||||
|             if((storage_file_get_error(directory) == FSE_OK) && (name_temp[0] != '\0')) { | ||||
|                 total_files_cnt++; | ||||
|                 furi_string_set(name_str, name_temp); | ||||
|                 if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { | ||||
|                 if(browser_filter_by_name(browser, name_str, file_info_is_dir(&file_info))) { | ||||
|                     if(!furi_string_empty(filename)) { | ||||
|                         if(furi_string_cmp(name_str, filename) == 0) { | ||||
|                             *file_idx = *item_cnt; | ||||
| @ -214,7 +214,7 @@ static bool | ||||
|             } | ||||
|             if(storage_file_get_error(directory) == FSE_OK) { | ||||
|                 furi_string_set(name_str, name_temp); | ||||
|                 if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { | ||||
|                 if(browser_filter_by_name(browser, name_str, file_info_is_dir(&file_info))) { | ||||
|                     items_cnt++; | ||||
|                 } | ||||
|             } else { | ||||
| @ -236,11 +236,11 @@ static bool | ||||
|             } | ||||
|             if(storage_file_get_error(directory) == FSE_OK) { | ||||
|                 furi_string_set(name_str, name_temp); | ||||
|                 if(browser_filter_by_name(browser, name_str, (file_info.flags & FSF_DIRECTORY))) { | ||||
|                 if(browser_filter_by_name(browser, name_str, file_info_is_dir(&file_info))) { | ||||
|                     furi_string_printf(name_str, "%s/%s", furi_string_get_cstr(path), name_temp); | ||||
|                     if(browser->list_item_cb) { | ||||
|                         browser->list_item_cb( | ||||
|                             browser->cb_ctx, name_str, (file_info.flags & FSF_DIRECTORY), false); | ||||
|                             browser->cb_ctx, name_str, file_info_is_dir(&file_info), false); | ||||
|                     } | ||||
|                     items_cnt++; | ||||
|                 } | ||||
|  | ||||
| @ -29,6 +29,8 @@ static bool | ||||
|     } | ||||
| 
 | ||||
|     furi_thread_set_name(loader_instance->application_thread, loader_instance->application->name); | ||||
|     furi_thread_set_appid( | ||||
|         loader_instance->application_thread, loader_instance->application->appid); | ||||
|     furi_thread_set_stack_size( | ||||
|         loader_instance->application_thread, loader_instance->application->stack_size); | ||||
|     furi_thread_set_context( | ||||
|  | ||||
| @ -201,7 +201,7 @@ static void rpc_system_storage_stat_process(const PB_Main* request, void* contex | ||||
|     if(error == FSE_OK) { | ||||
|         response->which_content = PB_Main_storage_stat_response_tag; | ||||
|         response->content.storage_stat_response.has_file = true; | ||||
|         response->content.storage_stat_response.file.type = (fileinfo.flags & FSF_DIRECTORY) ? | ||||
|         response->content.storage_stat_response.file.type = file_info_is_dir(&fileinfo) ? | ||||
|                                                                 PB_Storage_File_FileType_DIR : | ||||
|                                                                 PB_Storage_File_FileType_FILE; | ||||
|         response->content.storage_stat_response.file.size = fileinfo.size; | ||||
| @ -291,9 +291,8 @@ static void rpc_system_storage_list_process(const PB_Main* request, void* contex | ||||
|                     rpc_send_and_release(session, &response); | ||||
|                     i = 0; | ||||
|                 } | ||||
|                 list->file[i].type = (fileinfo.flags & FSF_DIRECTORY) ? | ||||
|                                          PB_Storage_File_FileType_DIR : | ||||
|                                          PB_Storage_File_FileType_FILE; | ||||
|                 list->file[i].type = file_info_is_dir(&fileinfo) ? PB_Storage_File_FileType_DIR : | ||||
|                                                                    PB_Storage_File_FileType_FILE; | ||||
|                 list->file[i].size = fileinfo.size; | ||||
|                 list->file[i].data = NULL; | ||||
|                 list->file[i].name = name; | ||||
| @ -458,7 +457,7 @@ static bool rpc_system_storage_is_dir_is_empty(Storage* fs_api, const char* path | ||||
|     FileInfo fileinfo; | ||||
|     bool is_dir_is_empty = true; | ||||
|     FS_Error error = storage_common_stat(fs_api, path, &fileinfo); | ||||
|     if((error == FSE_OK) && (fileinfo.flags & FSF_DIRECTORY)) { | ||||
|     if((error == FSE_OK) && file_info_is_dir(&fileinfo)) { | ||||
|         File* dir = storage_file_alloc(fs_api); | ||||
|         if(storage_dir_open(dir, path)) { | ||||
|             char* name = malloc(MAX_NAME_LENGTH); | ||||
|  | ||||
| @ -36,3 +36,7 @@ const char* filesystem_api_error_get_desc(FS_Error error_id) { | ||||
|     } | ||||
|     return result; | ||||
| } | ||||
| 
 | ||||
| bool file_info_is_dir(const FileInfo* file_info) { | ||||
|     return (file_info->flags & FSF_DIRECTORY); | ||||
| } | ||||
| @ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
| #include <stdint.h> | ||||
| #include <stdbool.h> | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
| @ -40,10 +41,10 @@ typedef enum { | ||||
|     FSF_DIRECTORY = (1 << 0), /**< Directory */ | ||||
| } FS_Flags; | ||||
| 
 | ||||
| /**  Structure that hold file index and returned api errors  */ | ||||
| /** Structure that hold file index and returned api errors  */ | ||||
| typedef struct File File; | ||||
| 
 | ||||
| /**  Structure that hold file info */ | ||||
| /** Structure that hold file info */ | ||||
| typedef struct { | ||||
|     uint8_t flags; /**< flags from FS_Flags enum */ | ||||
|     uint64_t size; /**< file size */ | ||||
| @ -55,6 +56,12 @@ typedef struct { | ||||
|  */ | ||||
| const char* filesystem_api_error_get_desc(FS_Error error_id); | ||||
| 
 | ||||
| /** Checks if file info is directory
 | ||||
|  * @param file_info file info pointer | ||||
|  * @return bool is directory | ||||
|  */ | ||||
| bool file_info_is_dir(const FileInfo* file_info); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @ -10,10 +10,12 @@ extern "C" { | ||||
| #define STORAGE_INT_PATH_PREFIX "/int" | ||||
| #define STORAGE_EXT_PATH_PREFIX "/ext" | ||||
| #define STORAGE_ANY_PATH_PREFIX "/any" | ||||
| #define STORAGE_APP_DATA_PATH_PREFIX "/app" | ||||
| 
 | ||||
| #define INT_PATH(path) STORAGE_INT_PATH_PREFIX "/" path | ||||
| #define EXT_PATH(path) STORAGE_EXT_PATH_PREFIX "/" path | ||||
| #define ANY_PATH(path) STORAGE_ANY_PATH_PREFIX "/" path | ||||
| #define APP_DATA_PATH(path) STORAGE_APP_DATA_PATH_PREFIX "/" path | ||||
| 
 | ||||
| #define RECORD_STORAGE "storage" | ||||
| 
 | ||||
| @ -175,6 +177,15 @@ bool storage_dir_read(File* file, FileInfo* fileinfo, char* name, uint16_t name_ | ||||
|  */ | ||||
| bool storage_dir_rewind(File* file); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Check that dir exists | ||||
|  *  | ||||
|  * @param storage  | ||||
|  * @param path  | ||||
|  * @return bool  | ||||
|  */ | ||||
| bool storage_dir_exists(Storage* storage, const char* path); | ||||
| 
 | ||||
| /******************* Common Functions *******************/ | ||||
| 
 | ||||
| /** Retrieves unix timestamp of last access
 | ||||
| @ -246,6 +257,36 @@ FS_Error storage_common_fs_info( | ||||
|     uint64_t* total_space, | ||||
|     uint64_t* free_space); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Parse aliases in path and replace them with real path | ||||
|  * Also will create special folders if they are not exist | ||||
|  *  | ||||
|  * @param storage  | ||||
|  * @param path  | ||||
|  * @return bool  | ||||
|  */ | ||||
| void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, FuriString* path); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Move content of one folder to another, with rename of all conflicting files.  | ||||
|  * Source folder will be deleted if the migration is successful. | ||||
|  *  | ||||
|  * @param storage  | ||||
|  * @param source  | ||||
|  * @param dest  | ||||
|  * @return FS_Error  | ||||
|  */ | ||||
| FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Check that file or dir exists | ||||
|  *  | ||||
|  * @param storage  | ||||
|  * @param path  | ||||
|  * @return bool  | ||||
|  */ | ||||
| bool storage_common_exists(Storage* storage, const char* path); | ||||
| 
 | ||||
| /******************* Error Functions *******************/ | ||||
| 
 | ||||
| /** Retrieves the error text from the error id
 | ||||
|  | ||||
| @ -131,7 +131,7 @@ static void storage_cli_list(Cli* cli, FuriString* path) { | ||||
| 
 | ||||
|             while(storage_dir_read(file, &fileinfo, name, MAX_NAME_LENGTH)) { | ||||
|                 read_done = true; | ||||
|                 if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|                 if(file_info_is_dir(&fileinfo)) { | ||||
|                     printf("\t[D] %s\r\n", name); | ||||
|                 } else { | ||||
|                     printf("\t[F] %s %lub\r\n", name, (uint32_t)(fileinfo.size)); | ||||
| @ -169,7 +169,7 @@ static void storage_cli_tree(Cli* cli, FuriString* path) { | ||||
| 
 | ||||
|             while(dir_walk_read(dir_walk, name, &fileinfo) == DirWalkOK) { | ||||
|                 read_done = true; | ||||
|                 if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|                 if(file_info_is_dir(&fileinfo)) { | ||||
|                     printf("\t[D] %s\r\n", furi_string_get_cstr(name)); | ||||
|                 } else { | ||||
|                     printf( | ||||
| @ -383,7 +383,7 @@ static void storage_cli_stat(Cli* cli, FuriString* path) { | ||||
|         FS_Error error = storage_common_stat(api, furi_string_get_cstr(path), &fileinfo); | ||||
| 
 | ||||
|         if(error == FSE_OK) { | ||||
|             if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|             if(file_info_is_dir(&fileinfo)) { | ||||
|                 printf("Directory\r\n"); | ||||
|             } else { | ||||
|                 printf("File, size: %lub\r\n", (uint32_t)(fileinfo.size)); | ||||
|  | ||||
| @ -39,12 +39,6 @@ | ||||
|             .file = file, \ | ||||
|         }}; | ||||
| 
 | ||||
| #define S_API_DATA_PATH   \ | ||||
|     SAData data = {       \ | ||||
|         .path = {         \ | ||||
|             .path = path, \ | ||||
|         }}; | ||||
| 
 | ||||
| #define S_RETURN_BOOL (return_data.bool_value); | ||||
| #define S_RETURN_UINT16 (return_data.uint16_value); | ||||
| #define S_RETURN_UINT64 (return_data.uint64_value); | ||||
| @ -70,6 +64,7 @@ static bool storage_file_open_internal( | ||||
|             .path = path, | ||||
|             .access_mode = access_mode, | ||||
|             .open_mode = open_mode, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     file->type = FileTypeOpenFile; | ||||
| @ -249,7 +244,7 @@ bool storage_file_exists(Storage* storage, const char* path) { | ||||
|     FileInfo fileinfo; | ||||
|     FS_Error error = storage_common_stat(storage, path, &fileinfo); | ||||
| 
 | ||||
|     if(error == FSE_OK && !(fileinfo.flags & FSF_DIRECTORY)) { | ||||
|     if(error == FSE_OK && !file_info_is_dir(&fileinfo)) { | ||||
|         exist = true; | ||||
|     } | ||||
| 
 | ||||
| @ -266,6 +261,7 @@ static bool storage_dir_open_internal(File* file, const char* path) { | ||||
|         .dopen = { | ||||
|             .file = file, | ||||
|             .path = path, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     file->type = FileTypeOpenDir; | ||||
| @ -349,12 +345,28 @@ bool storage_dir_rewind(File* file) { | ||||
|     return S_RETURN_BOOL; | ||||
| } | ||||
| 
 | ||||
| bool storage_dir_exists(Storage* storage, const char* path) { | ||||
|     bool exist = false; | ||||
|     FileInfo fileinfo; | ||||
|     FS_Error error = storage_common_stat(storage, path, &fileinfo); | ||||
| 
 | ||||
|     if(error == FSE_OK && file_info_is_dir(&fileinfo)) { | ||||
|         exist = true; | ||||
|     } | ||||
| 
 | ||||
|     return exist; | ||||
| } | ||||
| /****************** COMMON ******************/ | ||||
| 
 | ||||
| FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* timestamp) { | ||||
|     S_API_PROLOGUE; | ||||
| 
 | ||||
|     SAData data = {.ctimestamp = {.path = path, .timestamp = timestamp}}; | ||||
|     SAData data = { | ||||
|         .ctimestamp = { | ||||
|             .path = path, | ||||
|             .timestamp = timestamp, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     S_API_MESSAGE(StorageCommandCommonTimestamp); | ||||
|     S_API_EPILOGUE; | ||||
| @ -363,8 +375,12 @@ FS_Error storage_common_timestamp(Storage* storage, const char* path, uint32_t* | ||||
| 
 | ||||
| FS_Error storage_common_stat(Storage* storage, const char* path, FileInfo* fileinfo) { | ||||
|     S_API_PROLOGUE; | ||||
| 
 | ||||
|     SAData data = {.cstat = {.path = path, .fileinfo = fileinfo}}; | ||||
|     SAData data = { | ||||
|         .cstat = { | ||||
|             .path = path, | ||||
|             .fileinfo = fileinfo, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     S_API_MESSAGE(StorageCommandCommonStat); | ||||
|     S_API_EPILOGUE; | ||||
| @ -373,7 +389,12 @@ FS_Error storage_common_stat(Storage* storage, const char* path, FileInfo* filei | ||||
| 
 | ||||
| FS_Error storage_common_remove(Storage* storage, const char* path) { | ||||
|     S_API_PROLOGUE; | ||||
|     S_API_DATA_PATH; | ||||
|     SAData data = { | ||||
|         .path = { | ||||
|             .path = path, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     S_API_MESSAGE(StorageCommandCommonRemove); | ||||
|     S_API_EPILOGUE; | ||||
|     return S_RETURN_ERROR; | ||||
| @ -423,7 +444,7 @@ static FS_Error | ||||
|                 furi_string_right(path, strlen(old_path)); | ||||
|                 furi_string_printf(tmp_new_path, "%s%s", new_path, furi_string_get_cstr(path)); | ||||
| 
 | ||||
|                 if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|                 if(file_info_is_dir(&fileinfo)) { | ||||
|                     error = storage_common_mkdir(storage, furi_string_get_cstr(tmp_new_path)); | ||||
|                 } else { | ||||
|                     error = storage_common_copy( | ||||
| @ -452,7 +473,7 @@ FS_Error storage_common_copy(Storage* storage, const char* old_path, const char* | ||||
|     error = storage_common_stat(storage, old_path, &fileinfo); | ||||
| 
 | ||||
|     if(error == FSE_OK) { | ||||
|         if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|         if(file_info_is_dir(&fileinfo)) { | ||||
|             error = storage_copy_recursive(storage, old_path, new_path); | ||||
|         } else { | ||||
|             Stream* stream_from = file_stream_alloc(storage); | ||||
| @ -479,7 +500,7 @@ FS_Error storage_common_copy(Storage* storage, const char* old_path, const char* | ||||
| 
 | ||||
| static FS_Error | ||||
|     storage_merge_recursive(Storage* storage, const char* old_path, const char* new_path) { | ||||
|     FS_Error error = storage_common_mkdir(storage, new_path); | ||||
|     FS_Error error = FSE_OK; | ||||
|     DirWalk* dir_walk = dir_walk_alloc(storage); | ||||
|     FuriString *path, *file_basename, *tmp_new_path; | ||||
|     FileInfo fileinfo; | ||||
| @ -488,7 +509,7 @@ static FS_Error | ||||
|     tmp_new_path = furi_string_alloc(); | ||||
| 
 | ||||
|     do { | ||||
|         if((error != FSE_OK) && (error != FSE_EXIST)) break; | ||||
|         if(!storage_simply_mkdir(storage, new_path)) break; | ||||
| 
 | ||||
|         dir_walk_set_recursive(dir_walk, false); | ||||
|         if(!dir_walk_open(dir_walk, old_path)) { | ||||
| @ -508,13 +529,13 @@ static FS_Error | ||||
|                 path_extract_basename(furi_string_get_cstr(path), file_basename); | ||||
|                 path_concat(new_path, furi_string_get_cstr(file_basename), tmp_new_path); | ||||
| 
 | ||||
|                 if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|                 if(file_info_is_dir(&fileinfo)) { | ||||
|                     if(storage_common_stat( | ||||
|                            storage, furi_string_get_cstr(tmp_new_path), &fileinfo) == FSE_OK) { | ||||
|                         if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|                         if(file_info_is_dir(&fileinfo)) { | ||||
|                             error = | ||||
|                                 storage_common_mkdir(storage, furi_string_get_cstr(tmp_new_path)); | ||||
|                             if(error != FSE_OK) { | ||||
|                             if(error != FSE_OK && error != FSE_EXIST) { | ||||
|                                 break; | ||||
|                             } | ||||
|                         } | ||||
| @ -548,7 +569,7 @@ FS_Error storage_common_merge(Storage* storage, const char* old_path, const char | ||||
|     error = storage_common_stat(storage, old_path, &fileinfo); | ||||
| 
 | ||||
|     if(error == FSE_OK) { | ||||
|         if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|         if(file_info_is_dir(&fileinfo)) { | ||||
|             error = storage_merge_recursive(storage, old_path, new_path); | ||||
|         } else { | ||||
|             error = storage_common_stat(storage, new_path, &fileinfo); | ||||
| @ -556,7 +577,7 @@ FS_Error storage_common_merge(Storage* storage, const char* old_path, const char | ||||
|                 furi_string_set(new_path_next, new_path); | ||||
|                 FuriString* dir_path; | ||||
|                 FuriString* filename; | ||||
|                 char extension[MAX_EXT_LEN]; | ||||
|                 char extension[MAX_EXT_LEN] = {0}; | ||||
| 
 | ||||
|                 dir_path = furi_string_alloc(); | ||||
|                 filename = furi_string_alloc(); | ||||
| @ -608,7 +629,12 @@ FS_Error storage_common_merge(Storage* storage, const char* old_path, const char | ||||
| 
 | ||||
| FS_Error storage_common_mkdir(Storage* storage, const char* path) { | ||||
|     S_API_PROLOGUE; | ||||
|     S_API_DATA_PATH; | ||||
|     SAData data = { | ||||
|         .path = { | ||||
|             .path = path, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     S_API_MESSAGE(StorageCommandCommonMkDir); | ||||
|     S_API_EPILOGUE; | ||||
|     return S_RETURN_ERROR; | ||||
| @ -626,6 +652,7 @@ FS_Error storage_common_fs_info( | ||||
|             .fs_path = fs_path, | ||||
|             .total_space = total_space, | ||||
|             .free_space = free_space, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     S_API_MESSAGE(StorageCommandCommonFSInfo); | ||||
| @ -633,6 +660,38 @@ FS_Error storage_common_fs_info( | ||||
|     return S_RETURN_ERROR; | ||||
| } | ||||
| 
 | ||||
| void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, FuriString* path) { | ||||
|     S_API_PROLOGUE; | ||||
| 
 | ||||
|     SAData data = { | ||||
|         .cresolvepath = { | ||||
|             .path = path, | ||||
|             .thread_id = furi_thread_get_current_id(), | ||||
|         }}; | ||||
| 
 | ||||
|     S_API_MESSAGE(StorageCommandCommonResolvePath); | ||||
|     S_API_EPILOGUE; | ||||
| } | ||||
| 
 | ||||
| FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest) { | ||||
|     if(!storage_common_exists(storage, source)) { | ||||
|         return FSE_OK; | ||||
|     } | ||||
| 
 | ||||
|     FS_Error error = storage_common_merge(storage, source, dest); | ||||
| 
 | ||||
|     if(error == FSE_OK) { | ||||
|         storage_simply_remove_recursive(storage, source); | ||||
|     } | ||||
| 
 | ||||
|     return error; | ||||
| } | ||||
| 
 | ||||
| bool storage_common_exists(Storage* storage, const char* path) { | ||||
|     FileInfo file_info; | ||||
|     return storage_common_stat(storage, path, &file_info) == FSE_OK; | ||||
| } | ||||
| 
 | ||||
| /****************** ERROR ******************/ | ||||
| 
 | ||||
| const char* storage_error_get_desc(FS_Error error_id) { | ||||
| @ -750,7 +809,7 @@ bool storage_simply_remove_recursive(Storage* storage, const char* path) { | ||||
|         } | ||||
| 
 | ||||
|         while(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) { | ||||
|             if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|             if(file_info_is_dir(&fileinfo)) { | ||||
|                 furi_string_cat_printf(cur_dir, "/%s", name); | ||||
|                 go_deeper = true; | ||||
|                 break; | ||||
|  | ||||
| @ -5,21 +5,18 @@ | ||||
| 
 | ||||
| void storage_file_init(StorageFile* obj) { | ||||
|     obj->file = NULL; | ||||
|     obj->type = ST_ERROR; | ||||
|     obj->file_data = NULL; | ||||
|     obj->path = furi_string_alloc(); | ||||
| } | ||||
| 
 | ||||
| void storage_file_init_set(StorageFile* obj, const StorageFile* src) { | ||||
|     obj->file = src->file; | ||||
|     obj->type = src->type; | ||||
|     obj->file_data = src->file_data; | ||||
|     obj->path = furi_string_alloc_set(src->path); | ||||
| } | ||||
| 
 | ||||
| void storage_file_set(StorageFile* obj, const StorageFile* src) { //-V524
 | ||||
|     obj->file = src->file; | ||||
|     obj->type = src->type; | ||||
|     obj->file_data = src->file_data; | ||||
|     furi_string_set(obj->path, src->path); | ||||
| } | ||||
| @ -150,16 +147,10 @@ void* storage_get_storage_file_data(const File* file, StorageData* storage) { | ||||
|     return founded_file->file_data; | ||||
| } | ||||
| 
 | ||||
| void storage_push_storage_file( | ||||
|     File* file, | ||||
|     FuriString* path, | ||||
|     StorageType type, | ||||
|     StorageData* storage) { | ||||
| void storage_push_storage_file(File* file, FuriString* path, StorageData* storage) { | ||||
|     StorageFile* storage_file = StorageFileList_push_new(storage->files); | ||||
| 
 | ||||
|     file->file_id = (uint32_t)storage_file; | ||||
|     storage_file->file = file; | ||||
|     storage_file->type = type; | ||||
|     furi_string_set(storage_file->path, path); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -18,7 +18,6 @@ typedef struct { | ||||
| 
 | ||||
| typedef struct { | ||||
|     File* file; | ||||
|     StorageType type; | ||||
|     void* file_data; | ||||
|     FuriString* path; | ||||
| } StorageFile; | ||||
| @ -66,11 +65,7 @@ bool storage_path_already_open(FuriString* path, StorageFileList_t files); | ||||
| void storage_set_storage_file_data(const File* file, void* file_data, StorageData* storage); | ||||
| void* storage_get_storage_file_data(const File* file, StorageData* storage); | ||||
| 
 | ||||
| void storage_push_storage_file( | ||||
|     File* file, | ||||
|     FuriString* path, | ||||
|     StorageType type, | ||||
|     StorageData* storage); | ||||
| void storage_push_storage_file(File* file, FuriString* path, StorageData* storage); | ||||
| bool storage_pop_storage_file(File* file, StorageData* storage); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
|  | ||||
| @ -12,6 +12,8 @@ extern "C" { | ||||
| 
 | ||||
| #define STORAGE_COUNT (ST_INT + 1) | ||||
| 
 | ||||
| #define APPS_DATA_PATH EXT_PATH("apps_data") | ||||
| 
 | ||||
| typedef struct { | ||||
|     ViewPort* view_port; | ||||
|     bool enabled; | ||||
|  | ||||
| @ -11,6 +11,7 @@ typedef struct { | ||||
|     const char* path; | ||||
|     FS_AccessMode access_mode; | ||||
|     FS_OpenMode open_mode; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataFOpen; | ||||
| 
 | ||||
| typedef struct { | ||||
| @ -34,6 +35,7 @@ typedef struct { | ||||
| typedef struct { | ||||
|     File* file; | ||||
|     const char* path; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataDOpen; | ||||
| 
 | ||||
| typedef struct { | ||||
| @ -46,25 +48,34 @@ typedef struct { | ||||
| typedef struct { | ||||
|     const char* path; | ||||
|     uint32_t* timestamp; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataCTimestamp; | ||||
| 
 | ||||
| typedef struct { | ||||
|     const char* path; | ||||
|     FileInfo* fileinfo; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataCStat; | ||||
| 
 | ||||
| typedef struct { | ||||
|     const char* fs_path; | ||||
|     uint64_t* total_space; | ||||
|     uint64_t* free_space; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataCFSInfo; | ||||
| 
 | ||||
| typedef struct { | ||||
|     FuriString* path; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataCResolvePath; | ||||
| 
 | ||||
| typedef struct { | ||||
|     uint32_t id; | ||||
| } SADataError; | ||||
| 
 | ||||
| typedef struct { | ||||
|     const char* path; | ||||
|     FuriThreadId thread_id; | ||||
| } SADataPath; | ||||
| 
 | ||||
| typedef struct { | ||||
| @ -87,6 +98,7 @@ typedef union { | ||||
|     SADataCTimestamp ctimestamp; | ||||
|     SADataCStat cstat; | ||||
|     SADataCFSInfo cfsinfo; | ||||
|     SADataCResolvePath cresolvepath; | ||||
| 
 | ||||
|     SADataError error; | ||||
| 
 | ||||
| @ -128,6 +140,7 @@ typedef enum { | ||||
|     StorageCommandSDUnmount, | ||||
|     StorageCommandSDInfo, | ||||
|     StorageCommandSDStatus, | ||||
|     StorageCommandCommonResolvePath, | ||||
| } StorageCommand; | ||||
| 
 | ||||
| typedef struct { | ||||
|  | ||||
| @ -4,17 +4,11 @@ | ||||
| 
 | ||||
| #define FS_CALL(_storage, _fn) ret = _storage->fs_api->_fn; | ||||
| 
 | ||||
| static StorageData* storage_get_storage_by_type(Storage* app, StorageType type) { | ||||
|     furi_check(type == ST_EXT || type == ST_INT); | ||||
|     StorageData* storage = &app->storage[type]; | ||||
|     return storage; | ||||
| } | ||||
| 
 | ||||
| static bool storage_type_is_not_valid(StorageType type) { | ||||
| static bool storage_type_is_valid(StorageType type) { | ||||
| #ifdef FURI_RAM_EXEC | ||||
|     return type != ST_EXT; | ||||
|     return type == ST_EXT; | ||||
| #else | ||||
|     return type >= ST_ERROR; | ||||
|     return type < ST_ERROR; | ||||
| #endif | ||||
| } | ||||
| 
 | ||||
| @ -30,25 +24,21 @@ static StorageData* get_storage_by_file(File* file, StorageData* storages) { | ||||
|     return storage_data; | ||||
| } | ||||
| 
 | ||||
| static const char* remove_vfs(const char* path) { | ||||
|     return path + MIN(4u, strlen(path)); | ||||
| static const char* cstr_path_without_vfs_prefix(FuriString* path) { | ||||
|     const char* path_cstr = furi_string_get_cstr(path); | ||||
|     return path_cstr + MIN(4u, strlen(path_cstr)); | ||||
| } | ||||
| 
 | ||||
| static StorageType storage_get_type_by_path(Storage* app, const char* path) { | ||||
| static StorageType storage_get_type_by_path(FuriString* path) { | ||||
|     StorageType type = ST_ERROR; | ||||
|     if(memcmp(path, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { | ||||
|         type = ST_EXT; | ||||
|     } else if(memcmp(path, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { | ||||
|         type = ST_INT; | ||||
|     } else if(memcmp(path, STORAGE_ANY_PATH_PREFIX, strlen(STORAGE_ANY_PATH_PREFIX)) == 0) { | ||||
|         type = ST_ANY; | ||||
|     } | ||||
|     const char* path_cstr = furi_string_get_cstr(path); | ||||
| 
 | ||||
|     if(type == ST_ANY) { | ||||
|     if(memcmp(path_cstr, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) { | ||||
|         type = ST_EXT; | ||||
|     } else if(memcmp(path_cstr, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) { | ||||
|         type = ST_INT; | ||||
|         if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusOK) { | ||||
|             type = ST_EXT; | ||||
|         } | ||||
|     } else if(memcmp(path_cstr, STORAGE_ANY_PATH_PREFIX, strlen(STORAGE_ANY_PATH_PREFIX)) == 0) { | ||||
|         type = ST_ANY; | ||||
|     } | ||||
| 
 | ||||
|     return type; | ||||
| @ -71,38 +61,51 @@ static void storage_path_change_to_real_storage(FuriString* path, StorageType re | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| FS_Error storage_get_data(Storage* app, FuriString* path, StorageData** storage) { | ||||
|     StorageType type = storage_get_type_by_path(path); | ||||
| 
 | ||||
|     if(storage_type_is_valid(type)) { | ||||
|         if(type == ST_ANY) { | ||||
|             type = ST_INT; | ||||
|             if(storage_data_status(&app->storage[ST_EXT]) == StorageStatusOK) { | ||||
|                 type = ST_EXT; | ||||
|             } | ||||
|             storage_path_change_to_real_storage(path, type); | ||||
|         } | ||||
| 
 | ||||
|         furi_assert(type == ST_EXT || type == ST_INT); | ||||
|         *storage = &app->storage[type]; | ||||
| 
 | ||||
|         return FSE_OK; | ||||
|     } else { | ||||
|         return FSE_INVALID_NAME; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /******************* File Functions *******************/ | ||||
| 
 | ||||
| bool storage_process_file_open( | ||||
|     Storage* app, | ||||
|     File* file, | ||||
|     const char* path, | ||||
|     FuriString* path, | ||||
|     FS_AccessMode access_mode, | ||||
|     FS_OpenMode open_mode) { | ||||
|     bool ret = false; | ||||
|     StorageType type = storage_get_type_by_path(app, path); | ||||
|     StorageData* storage; | ||||
|     file->error_id = FSE_OK; | ||||
|     file->error_id = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     if(storage_type_is_not_valid(type)) { | ||||
|         file->error_id = FSE_INVALID_NAME; | ||||
|     } else { | ||||
|         storage = storage_get_storage_by_type(app, type); | ||||
|         FuriString* real_path; | ||||
|         real_path = furi_string_alloc_set(path); | ||||
|         storage_path_change_to_real_storage(real_path, type); | ||||
| 
 | ||||
|         if(storage_path_already_open(real_path, storage->files)) { | ||||
|     if(file->error_id == FSE_OK) { | ||||
|         if(storage_path_already_open(path, storage->files)) { | ||||
|             file->error_id = FSE_ALREADY_OPEN; | ||||
|         } else { | ||||
|             if(access_mode & FSAM_WRITE) { | ||||
|                 storage_data_timestamp(storage); | ||||
|             } | ||||
|             storage_push_storage_file(file, real_path, type, storage); | ||||
|             FS_CALL(storage, file.open(storage, file, remove_vfs(path), access_mode, open_mode)); | ||||
|         } | ||||
|             storage_push_storage_file(file, path, storage); | ||||
| 
 | ||||
|         furi_string_free(real_path); | ||||
|             const char* path_cstr_no_vfs = cstr_path_without_vfs_prefix(path); | ||||
|             FS_CALL(storage, file.open(storage, file, path_cstr_no_vfs, access_mode, open_mode)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return ret; | ||||
| @ -243,27 +246,18 @@ static bool storage_process_file_eof(Storage* app, File* file) { | ||||
| 
 | ||||
| /******************* Dir Functions *******************/ | ||||
| 
 | ||||
| bool storage_process_dir_open(Storage* app, File* file, const char* path) { | ||||
| bool storage_process_dir_open(Storage* app, File* file, FuriString* path) { | ||||
|     bool ret = false; | ||||
|     StorageType type = storage_get_type_by_path(app, path); | ||||
|     StorageData* storage; | ||||
|     file->error_id = FSE_OK; | ||||
|     file->error_id = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     if(storage_type_is_not_valid(type)) { | ||||
|         file->error_id = FSE_INVALID_NAME; | ||||
|     } else { | ||||
|         storage = storage_get_storage_by_type(app, type); | ||||
|         FuriString* real_path; | ||||
|         real_path = furi_string_alloc_set(path); | ||||
|         storage_path_change_to_real_storage(real_path, type); | ||||
| 
 | ||||
|         if(storage_path_already_open(real_path, storage->files)) { | ||||
|     if(file->error_id == FSE_OK) { | ||||
|         if(storage_path_already_open(path, storage->files)) { | ||||
|             file->error_id = FSE_ALREADY_OPEN; | ||||
|         } else { | ||||
|             storage_push_storage_file(file, real_path, type, storage); | ||||
|             FS_CALL(storage, dir.open(storage, file, remove_vfs(path))); | ||||
|             storage_push_storage_file(file, path, storage); | ||||
|             FS_CALL(storage, dir.open(storage, file, cstr_path_without_vfs_prefix(path))); | ||||
|         } | ||||
|         furi_string_free(real_path); | ||||
|     } | ||||
| 
 | ||||
|     return ret; | ||||
| @ -320,73 +314,52 @@ bool storage_process_dir_rewind(Storage* app, File* file) { | ||||
| /******************* Common FS Functions *******************/ | ||||
| 
 | ||||
| static FS_Error | ||||
|     storage_process_common_timestamp(Storage* app, const char* path, uint32_t* timestamp) { | ||||
|     FS_Error ret = FSE_OK; | ||||
|     StorageType type = storage_get_type_by_path(app, path); | ||||
|     storage_process_common_timestamp(Storage* app, FuriString* path, uint32_t* timestamp) { | ||||
|     StorageData* storage; | ||||
|     FS_Error ret = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     if(storage_type_is_not_valid(type)) { | ||||
|         ret = FSE_INVALID_NAME; | ||||
|     } else { | ||||
|         StorageData* storage = storage_get_storage_by_type(app, type); | ||||
|     if(ret == FSE_OK) { | ||||
|         *timestamp = storage_data_get_timestamp(storage); | ||||
|     } | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| static FS_Error storage_process_common_stat(Storage* app, const char* path, FileInfo* fileinfo) { | ||||
|     FS_Error ret = FSE_OK; | ||||
|     StorageType type = storage_get_type_by_path(app, path); | ||||
| static FS_Error storage_process_common_stat(Storage* app, FuriString* path, FileInfo* fileinfo) { | ||||
|     StorageData* storage; | ||||
|     FS_Error ret = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     if(storage_type_is_not_valid(type)) { | ||||
|         ret = FSE_INVALID_NAME; | ||||
|     } else { | ||||
|         StorageData* storage = storage_get_storage_by_type(app, type); | ||||
|         FS_CALL(storage, common.stat(storage, remove_vfs(path), fileinfo)); | ||||
|     if(ret == FSE_OK) { | ||||
|         FS_CALL(storage, common.stat(storage, cstr_path_without_vfs_prefix(path), fileinfo)); | ||||
|     } | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| static FS_Error storage_process_common_remove(Storage* app, const char* path) { | ||||
|     FS_Error ret = FSE_OK; | ||||
|     StorageType type = storage_get_type_by_path(app, path); | ||||
| 
 | ||||
|     FuriString* real_path; | ||||
|     real_path = furi_string_alloc_set(path); | ||||
|     storage_path_change_to_real_storage(real_path, type); | ||||
| static FS_Error storage_process_common_remove(Storage* app, FuriString* path) { | ||||
|     StorageData* storage; | ||||
|     FS_Error ret = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     do { | ||||
|         if(storage_type_is_not_valid(type)) { | ||||
|             ret = FSE_INVALID_NAME; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         StorageData* storage = storage_get_storage_by_type(app, type); | ||||
|         if(storage_path_already_open(real_path, storage->files)) { | ||||
|         if(storage_path_already_open(path, storage->files)) { | ||||
|             ret = FSE_ALREADY_OPEN; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|         storage_data_timestamp(storage); | ||||
|         FS_CALL(storage, common.remove(storage, remove_vfs(path))); | ||||
|         FS_CALL(storage, common.remove(storage, cstr_path_without_vfs_prefix(path))); | ||||
|     } while(false); | ||||
| 
 | ||||
|     furi_string_free(real_path); | ||||
| 
 | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| static FS_Error storage_process_common_mkdir(Storage* app, const char* path) { | ||||
|     FS_Error ret = FSE_OK; | ||||
|     StorageType type = storage_get_type_by_path(app, path); | ||||
| static FS_Error storage_process_common_mkdir(Storage* app, FuriString* path) { | ||||
|     StorageData* storage; | ||||
|     FS_Error ret = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     if(storage_type_is_not_valid(type)) { | ||||
|         ret = FSE_INVALID_NAME; | ||||
|     } else { | ||||
|         StorageData* storage = storage_get_storage_by_type(app, type); | ||||
|     if(ret == FSE_OK) { | ||||
|         storage_data_timestamp(storage); | ||||
|         FS_CALL(storage, common.mkdir(storage, remove_vfs(path))); | ||||
|         FS_CALL(storage, common.mkdir(storage, cstr_path_without_vfs_prefix(path))); | ||||
|     } | ||||
| 
 | ||||
|     return ret; | ||||
| @ -394,17 +367,16 @@ static FS_Error storage_process_common_mkdir(Storage* app, const char* path) { | ||||
| 
 | ||||
| static FS_Error storage_process_common_fs_info( | ||||
|     Storage* app, | ||||
|     const char* fs_path, | ||||
|     FuriString* path, | ||||
|     uint64_t* total_space, | ||||
|     uint64_t* free_space) { | ||||
|     FS_Error ret = FSE_OK; | ||||
|     StorageType type = storage_get_type_by_path(app, fs_path); | ||||
|     StorageData* storage; | ||||
|     FS_Error ret = storage_get_data(app, path, &storage); | ||||
| 
 | ||||
|     if(storage_type_is_not_valid(type)) { | ||||
|         ret = FSE_INVALID_NAME; | ||||
|     } else { | ||||
|         StorageData* storage = storage_get_storage_by_type(app, type); | ||||
|         FS_CALL(storage, common.fs_info(storage, remove_vfs(fs_path), total_space, free_space)); | ||||
|     if(ret == FSE_OK) { | ||||
|         FS_CALL( | ||||
|             storage, | ||||
|             common.fs_info(storage, cstr_path_without_vfs_prefix(path), total_space, free_space)); | ||||
|     } | ||||
| 
 | ||||
|     return ret; | ||||
| @ -471,14 +443,52 @@ static FS_Error storage_process_sd_status(Storage* app) { | ||||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| /******************** Aliases processing *******************/ | ||||
| 
 | ||||
| void storage_process_alias( | ||||
|     Storage* app, | ||||
|     FuriString* path, | ||||
|     FuriThreadId thread_id, | ||||
|     bool create_folders) { | ||||
|     if(furi_string_start_with(path, STORAGE_APP_DATA_PATH_PREFIX)) { | ||||
|         FuriString* apps_data_path_with_appsid = furi_string_alloc_set(APPS_DATA_PATH "/"); | ||||
|         furi_string_cat(apps_data_path_with_appsid, furi_thread_get_appid(thread_id)); | ||||
| 
 | ||||
|         // "/app" -> "/ext/apps_data/appsid"
 | ||||
|         furi_string_replace_at( | ||||
|             path, | ||||
|             0, | ||||
|             strlen(STORAGE_APP_DATA_PATH_PREFIX), | ||||
|             furi_string_get_cstr(apps_data_path_with_appsid)); | ||||
| 
 | ||||
|         // Create app data folder if not exists
 | ||||
|         if(create_folders && | ||||
|            storage_process_common_stat(app, apps_data_path_with_appsid, NULL) != FSE_OK) { | ||||
|             furi_string_set(apps_data_path_with_appsid, APPS_DATA_PATH); | ||||
|             storage_process_common_mkdir(app, apps_data_path_with_appsid); | ||||
|             furi_string_cat(apps_data_path_with_appsid, "/"); | ||||
|             furi_string_cat(apps_data_path_with_appsid, furi_thread_get_appid(thread_id)); | ||||
|             storage_process_common_mkdir(app, apps_data_path_with_appsid); | ||||
|         } | ||||
| 
 | ||||
|         furi_string_free(apps_data_path_with_appsid); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /****************** API calls processing ******************/ | ||||
| 
 | ||||
| void storage_process_message_internal(Storage* app, StorageMessage* message) { | ||||
|     FuriString* path = NULL; | ||||
| 
 | ||||
|     switch(message->command) { | ||||
|     // File operations
 | ||||
|     case StorageCommandFileOpen: | ||||
|         path = furi_string_alloc_set(message->data->fopen.path); | ||||
|         storage_process_alias(app, path, message->data->fopen.thread_id, true); | ||||
|         message->return_data->bool_value = storage_process_file_open( | ||||
|             app, | ||||
|             message->data->fopen.file, | ||||
|             message->data->fopen.path, | ||||
|             path, | ||||
|             message->data->fopen.access_mode, | ||||
|             message->data->fopen.open_mode); | ||||
|         break; | ||||
| @ -527,9 +537,12 @@ void storage_process_message_internal(Storage* app, StorageMessage* message) { | ||||
|         message->return_data->bool_value = storage_process_file_eof(app, message->data->file.file); | ||||
|         break; | ||||
| 
 | ||||
|     // Dir operations
 | ||||
|     case StorageCommandDirOpen: | ||||
|         path = furi_string_alloc_set(message->data->dopen.path); | ||||
|         storage_process_alias(app, path, message->data->dopen.thread_id, true); | ||||
|         message->return_data->bool_value = | ||||
|             storage_process_dir_open(app, message->data->dopen.file, message->data->dopen.path); | ||||
|             storage_process_dir_open(app, message->data->dopen.file, path); | ||||
|         break; | ||||
|     case StorageCommandDirClose: | ||||
|         message->return_data->bool_value = | ||||
| @ -547,29 +560,42 @@ void storage_process_message_internal(Storage* app, StorageMessage* message) { | ||||
|         message->return_data->bool_value = | ||||
|             storage_process_dir_rewind(app, message->data->file.file); | ||||
|         break; | ||||
| 
 | ||||
|     // Common operations
 | ||||
|     case StorageCommandCommonTimestamp: | ||||
|         message->return_data->error_value = storage_process_common_timestamp( | ||||
|             app, message->data->ctimestamp.path, message->data->ctimestamp.timestamp); | ||||
|         path = furi_string_alloc_set(message->data->ctimestamp.path); | ||||
|         storage_process_alias(app, path, message->data->ctimestamp.thread_id, false); | ||||
|         message->return_data->error_value = | ||||
|             storage_process_common_timestamp(app, path, message->data->ctimestamp.timestamp); | ||||
|         break; | ||||
|     case StorageCommandCommonStat: | ||||
|         message->return_data->error_value = storage_process_common_stat( | ||||
|             app, message->data->cstat.path, message->data->cstat.fileinfo); | ||||
|         path = furi_string_alloc_set(message->data->cstat.path); | ||||
|         storage_process_alias(app, path, message->data->cstat.thread_id, false); | ||||
|         message->return_data->error_value = | ||||
|             storage_process_common_stat(app, path, message->data->cstat.fileinfo); | ||||
|         break; | ||||
|     case StorageCommandCommonRemove: | ||||
|         message->return_data->error_value = | ||||
|             storage_process_common_remove(app, message->data->path.path); | ||||
|         path = furi_string_alloc_set(message->data->path.path); | ||||
|         storage_process_alias(app, path, message->data->path.thread_id, false); | ||||
|         message->return_data->error_value = storage_process_common_remove(app, path); | ||||
|         break; | ||||
|     case StorageCommandCommonMkDir: | ||||
|         message->return_data->error_value = | ||||
|             storage_process_common_mkdir(app, message->data->path.path); | ||||
|         path = furi_string_alloc_set(message->data->path.path); | ||||
|         storage_process_alias(app, path, message->data->path.thread_id, true); | ||||
|         message->return_data->error_value = storage_process_common_mkdir(app, path); | ||||
|         break; | ||||
|     case StorageCommandCommonFSInfo: | ||||
|         path = furi_string_alloc_set(message->data->cfsinfo.fs_path); | ||||
|         storage_process_alias(app, path, message->data->cfsinfo.thread_id, false); | ||||
|         message->return_data->error_value = storage_process_common_fs_info( | ||||
|             app, | ||||
|             message->data->cfsinfo.fs_path, | ||||
|             message->data->cfsinfo.total_space, | ||||
|             message->data->cfsinfo.free_space); | ||||
|             app, path, message->data->cfsinfo.total_space, message->data->cfsinfo.free_space); | ||||
|         break; | ||||
|     case StorageCommandCommonResolvePath: | ||||
|         storage_process_alias( | ||||
|             app, message->data->cresolvepath.path, message->data->cresolvepath.thread_id, true); | ||||
|         break; | ||||
| 
 | ||||
|     // SD operations
 | ||||
|     case StorageCommandSDFormat: | ||||
|         message->return_data->error_value = storage_process_sd_format(app); | ||||
|         break; | ||||
| @ -585,6 +611,10 @@ void storage_process_message_internal(Storage* app, StorageMessage* message) { | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     if(path != NULL) { //-V547
 | ||||
|         furi_string_free(path); | ||||
|     } | ||||
| 
 | ||||
|     api_lock_unlock(message->lock); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,341 +0,0 @@ | ||||
| #include <furi.h> | ||||
| #include <furi_hal.h> | ||||
| #include <storage/storage.h> | ||||
| 
 | ||||
| #define TAG "StorageTest" | ||||
| #define BYTES_COUNT 16 | ||||
| #define TEST_STRING "TestDataStringProvidedByDiceRoll" | ||||
| #define SEEK_OFFSET_FROM_START 10 | ||||
| #define SEEK_OFFSET_INCREASE 12 | ||||
| #define SEEK_OFFSET_SUM (SEEK_OFFSET_FROM_START + SEEK_OFFSET_INCREASE) | ||||
| 
 | ||||
| static void do_file_test(Storage* api, const char* path) { | ||||
|     File* file = storage_file_alloc(api); | ||||
|     bool result; | ||||
|     uint8_t bytes[BYTES_COUNT + 1]; | ||||
|     uint8_t bytes_count; | ||||
|     uint64_t position; | ||||
|     uint64_t size; | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "--------- FILE \"%s\" ---------", path); | ||||
| 
 | ||||
|     // open
 | ||||
|     result = storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "open"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "open, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // write
 | ||||
|     bytes_count = storage_file_write(file, TEST_STRING, strlen(TEST_STRING)); | ||||
|     if(bytes_count == 0) { | ||||
|         FURI_LOG_E(TAG, "write, %s", storage_file_get_error_desc(file)); | ||||
|     } else { | ||||
|         FURI_LOG_I(TAG, "write"); | ||||
|     } | ||||
| 
 | ||||
|     // sync
 | ||||
|     result = storage_file_sync(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "sync"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "sync, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // eof #1
 | ||||
|     result = storage_file_eof(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "eof #1"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "eof #1, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // seek from start and tell
 | ||||
|     result = storage_file_seek(file, SEEK_OFFSET_FROM_START, true); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "seek #1"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "seek #1, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
|     position = storage_file_tell(file); | ||||
|     if(position != SEEK_OFFSET_FROM_START) { | ||||
|         FURI_LOG_E(TAG, "tell #1, %s", storage_file_get_error_desc(file)); | ||||
|     } else { | ||||
|         FURI_LOG_I(TAG, "tell #1"); | ||||
|     } | ||||
| 
 | ||||
|     // size
 | ||||
|     size = storage_file_size(file); | ||||
|     if(size != strlen(TEST_STRING)) { | ||||
|         FURI_LOG_E(TAG, "size #1, %s", storage_file_get_error_desc(file)); | ||||
|     } else { | ||||
|         FURI_LOG_I(TAG, "size #1"); | ||||
|     } | ||||
| 
 | ||||
|     // seek and tell
 | ||||
|     result = storage_file_seek(file, SEEK_OFFSET_INCREASE, false); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "seek #2"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "seek #2, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
|     position = storage_file_tell(file); | ||||
|     if(position != SEEK_OFFSET_SUM) { | ||||
|         FURI_LOG_E(TAG, "tell #2, %s", storage_file_get_error_desc(file)); | ||||
|     } else { | ||||
|         FURI_LOG_I(TAG, "tell #2"); | ||||
|     } | ||||
| 
 | ||||
|     // eof #2
 | ||||
|     result = storage_file_eof(file); | ||||
|     if(!result) { | ||||
|         FURI_LOG_I(TAG, "eof #2"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "eof #2, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // truncate
 | ||||
|     result = storage_file_truncate(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "truncate"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "truncate, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
|     size = storage_file_size(file); | ||||
|     if(size != SEEK_OFFSET_SUM) { | ||||
|         FURI_LOG_E(TAG, "size #2, %s", storage_file_get_error_desc(file)); | ||||
|     } else { | ||||
|         FURI_LOG_I(TAG, "size #2"); | ||||
|     } | ||||
| 
 | ||||
|     // close
 | ||||
|     result = storage_file_close(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "close"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "close, error"); | ||||
|     } | ||||
| 
 | ||||
|     // open
 | ||||
|     result = storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "open"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "open, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // read
 | ||||
|     memset(bytes, 0, BYTES_COUNT + 1); | ||||
|     bytes_count = storage_file_read(file, bytes, BYTES_COUNT); | ||||
|     if(bytes_count == 0) { | ||||
|         FURI_LOG_E(TAG, "read, %s", storage_file_get_error_desc(file)); | ||||
|     } else { | ||||
|         if(memcmp(TEST_STRING, bytes, bytes_count) == 0) { | ||||
|             FURI_LOG_I(TAG, "read"); | ||||
|         } else { | ||||
|             FURI_LOG_E(TAG, "read, garbage"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // close
 | ||||
|     result = storage_file_close(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "close"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "close, error"); | ||||
|     } | ||||
| 
 | ||||
|     storage_file_free(file); | ||||
| } | ||||
| 
 | ||||
| static void do_dir_test(Storage* api, const char* path) { | ||||
|     File* file = storage_file_alloc(api); | ||||
|     bool result; | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "--------- DIR \"%s\" ---------", path); | ||||
| 
 | ||||
|     // open
 | ||||
|     result = storage_dir_open(file, path); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "open"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "open, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // read
 | ||||
|     const uint8_t filename_size = 100; | ||||
|     char* filename = malloc(filename_size); | ||||
|     FileInfo fileinfo; | ||||
| 
 | ||||
|     do { | ||||
|         result = storage_dir_read(file, &fileinfo, filename, filename_size); | ||||
|         if(result) { | ||||
|             if(strlen(filename)) { | ||||
|                 FURI_LOG_I( | ||||
|                     TAG, | ||||
|                     "read #1, [%s]%s", | ||||
|                     ((fileinfo.flags & FSF_DIRECTORY) ? "D" : "F"), | ||||
|                     filename); | ||||
|             } | ||||
|         } else if(storage_file_get_error(file) != FSE_NOT_EXIST) { | ||||
|             FURI_LOG_E(TAG, "read #1, %s", storage_file_get_error_desc(file)); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|     } while(result); | ||||
| 
 | ||||
|     // rewind
 | ||||
|     result = storage_dir_rewind(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "rewind"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "rewind, %s", storage_file_get_error_desc(file)); | ||||
|     } | ||||
| 
 | ||||
|     // read
 | ||||
|     do { | ||||
|         result = storage_dir_read(file, &fileinfo, filename, filename_size); | ||||
|         if(result) { | ||||
|             if(strlen(filename)) { | ||||
|                 FURI_LOG_I( | ||||
|                     TAG, | ||||
|                     "read #2, [%s]%s", | ||||
|                     ((fileinfo.flags & FSF_DIRECTORY) ? "D" : "F"), | ||||
|                     filename); | ||||
|             } | ||||
|         } else if(storage_file_get_error(file) != FSE_NOT_EXIST) { | ||||
|             FURI_LOG_E(TAG, "read #2, %s", storage_file_get_error_desc(file)); | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|     } while((strlen(filename))); | ||||
| 
 | ||||
|     // close
 | ||||
|     result = storage_dir_close(file); | ||||
|     if(result) { | ||||
|         FURI_LOG_I(TAG, "close"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "close, error"); | ||||
|     } | ||||
| 
 | ||||
|     storage_file_free(file); | ||||
|     free(filename); | ||||
| } | ||||
| 
 | ||||
| static void do_test_start(Storage* api, const char* path) { | ||||
|     FuriString* str_path = furi_string_alloc_printf("%s/test-folder", path); | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "--------- START \"%s\" ---------", path); | ||||
| 
 | ||||
|     // mkdir
 | ||||
|     FS_Error result = storage_common_mkdir(api, furi_string_get_cstr(str_path)); | ||||
| 
 | ||||
|     if(result == FSE_OK) { | ||||
|         FURI_LOG_I(TAG, "mkdir ok"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "mkdir, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     // stat
 | ||||
|     FileInfo fileinfo; | ||||
|     result = storage_common_stat(api, furi_string_get_cstr(str_path), &fileinfo); | ||||
| 
 | ||||
|     if(result == FSE_OK) { | ||||
|         if(fileinfo.flags & FSF_DIRECTORY) { | ||||
|             FURI_LOG_I(TAG, "stat #1 ok"); | ||||
|         } else { | ||||
|             FURI_LOG_E(TAG, "stat #1, %s", storage_error_get_desc(result)); | ||||
|         } | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "stat #1, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     furi_string_free(str_path); | ||||
| } | ||||
| 
 | ||||
| static void do_test_end(Storage* api, const char* path) { | ||||
|     uint64_t total_space; | ||||
|     uint64_t free_space; | ||||
|     FuriString* str_path_1 = furi_string_alloc_printf("%s/test-folder", path); | ||||
|     FuriString* str_path_2 = furi_string_alloc_printf("%s/test-folder2", path); | ||||
| 
 | ||||
|     FURI_LOG_I(TAG, "--------- END \"%s\" ---------", path); | ||||
| 
 | ||||
|     // fs stat
 | ||||
|     FS_Error result = storage_common_fs_info(api, path, &total_space, &free_space); | ||||
| 
 | ||||
|     if(result == FSE_OK) { | ||||
|         uint32_t total_kb = total_space / 1024; | ||||
|         uint32_t free_kb = free_space / 1024; | ||||
|         FURI_LOG_I(TAG, "fs_info: total %luk, free %luk", total_kb, free_kb); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "fs_info, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     // rename #1
 | ||||
|     result = storage_common_rename( | ||||
|         api, furi_string_get_cstr(str_path_1), furi_string_get_cstr(str_path_2)); | ||||
|     if(result == FSE_OK) { | ||||
|         FURI_LOG_I(TAG, "rename #1 ok"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "rename #1, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     // remove #1
 | ||||
|     result = storage_common_remove(api, furi_string_get_cstr(str_path_2)); | ||||
|     if(result == FSE_OK) { | ||||
|         FURI_LOG_I(TAG, "remove #1 ok"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "remove #1, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     // rename #2
 | ||||
|     furi_string_printf(str_path_1, "%s/test.txt", path); | ||||
|     furi_string_printf(str_path_2, "%s/test2.txt", path); | ||||
| 
 | ||||
|     result = storage_common_rename( | ||||
|         api, furi_string_get_cstr(str_path_1), furi_string_get_cstr(str_path_2)); | ||||
|     if(result == FSE_OK) { | ||||
|         FURI_LOG_I(TAG, "rename #2 ok"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "rename #2, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     // remove #2
 | ||||
|     result = storage_common_remove(api, furi_string_get_cstr(str_path_2)); | ||||
|     if(result == FSE_OK) { | ||||
|         FURI_LOG_I(TAG, "remove #2 ok"); | ||||
|     } else { | ||||
|         FURI_LOG_E(TAG, "remove #2, %s", storage_error_get_desc(result)); | ||||
|     } | ||||
| 
 | ||||
|     furi_string_free(str_path_1); | ||||
|     furi_string_free(str_path_2); | ||||
| } | ||||
| 
 | ||||
| int32_t storage_test_app(void* p) { | ||||
|     UNUSED(p); | ||||
|     Storage* api = furi_record_open(RECORD_STORAGE); | ||||
|     do_test_start(api, STORAGE_INT_PATH_PREFIX); | ||||
|     do_test_start(api, STORAGE_ANY_PATH_PREFIX); | ||||
|     do_test_start(api, STORAGE_EXT_PATH_PREFIX); | ||||
| 
 | ||||
|     do_file_test(api, INT_PATH("test.txt")); | ||||
|     do_file_test(api, ANY_PATH("test.txt")); | ||||
|     do_file_test(api, EXT_PATH("test.txt")); | ||||
| 
 | ||||
|     do_dir_test(api, STORAGE_INT_PATH_PREFIX); | ||||
|     do_dir_test(api, STORAGE_ANY_PATH_PREFIX); | ||||
|     do_dir_test(api, STORAGE_EXT_PATH_PREFIX); | ||||
| 
 | ||||
|     do_test_end(api, STORAGE_INT_PATH_PREFIX); | ||||
|     do_test_end(api, STORAGE_ANY_PATH_PREFIX); | ||||
|     do_test_end(api, STORAGE_EXT_PATH_PREFIX); | ||||
| 
 | ||||
|     while(true) { | ||||
|         furi_delay_ms(1000); | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| @ -13,7 +13,7 @@ | ||||
| 
 | ||||
| static bool storage_move_to_sd_check_entry(const char* name, FileInfo* fileinfo, void* ctx) { | ||||
|     UNUSED(ctx); | ||||
|     if((fileinfo->flags & FSF_DIRECTORY) != 0) { | ||||
|     if(file_info_is_dir(fileinfo)) { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| entry,status,name,type,params | ||||
| Version,+,15.0,, | ||||
| Version,+,15.1,, | ||||
| Header,+,applications/services/bt/bt_service/bt.h,, | ||||
| Header,+,applications/services/cli/cli.h,, | ||||
| Header,+,applications/services/cli/cli_vcp.h,, | ||||
| @ -847,6 +847,7 @@ Function,+,file_browser_worker_set_folder_callback,void,"BrowserWorker*, Browser | ||||
| Function,+,file_browser_worker_set_item_callback,void,"BrowserWorker*, BrowserWorkerListItemCallback" | ||||
| Function,+,file_browser_worker_set_list_callback,void,"BrowserWorker*, BrowserWorkerListLoadCallback" | ||||
| Function,+,file_browser_worker_set_long_load_callback,void,"BrowserWorker*, BrowserWorkerLongLoadCallback" | ||||
| Function,+,file_info_is_dir,_Bool,const FileInfo* | ||||
| Function,+,file_stream_alloc,Stream*,Storage* | ||||
| Function,+,file_stream_close,_Bool,Stream* | ||||
| Function,+,file_stream_get_error,FS_Error,Stream* | ||||
| @ -1521,6 +1522,7 @@ Function,+,furi_thread_flags_get,uint32_t, | ||||
| Function,+,furi_thread_flags_set,uint32_t,"FuriThreadId, uint32_t" | ||||
| Function,+,furi_thread_flags_wait,uint32_t,"uint32_t, uint32_t, uint32_t" | ||||
| Function,+,furi_thread_free,void,FuriThread* | ||||
| Function,+,furi_thread_get_appid,const char*,FuriThreadId | ||||
| Function,+,furi_thread_get_current,FuriThread*, | ||||
| Function,+,furi_thread_get_current_id,FuriThreadId, | ||||
| Function,+,furi_thread_get_current_priority,FuriThreadPriority, | ||||
| @ -1535,6 +1537,7 @@ Function,+,furi_thread_is_suspended,_Bool,FuriThreadId | ||||
| Function,+,furi_thread_join,_Bool,FuriThread* | ||||
| Function,+,furi_thread_mark_as_service,void,FuriThread* | ||||
| Function,+,furi_thread_resume,void,FuriThreadId | ||||
| Function,+,furi_thread_set_appid,void,"FuriThread*, const char*" | ||||
| Function,+,furi_thread_set_callback,void,"FuriThread*, FuriThreadCallback" | ||||
| Function,+,furi_thread_set_context,void,"FuriThread*, void*" | ||||
| Function,+,furi_thread_set_current_priority,void,FuriThreadPriority | ||||
| @ -2430,14 +2433,18 @@ Function,-,srand48,void,long | ||||
| Function,-,srandom,void,unsigned | ||||
| Function,+,sscanf,int,"const char*, const char*, ..." | ||||
| Function,+,storage_common_copy,FS_Error,"Storage*, const char*, const char*" | ||||
| Function,+,storage_common_exists,_Bool,"Storage*, const char*" | ||||
| Function,+,storage_common_fs_info,FS_Error,"Storage*, const char*, uint64_t*, uint64_t*" | ||||
| Function,+,storage_common_merge,FS_Error,"Storage*, const char*, const char*" | ||||
| Function,+,storage_common_migrate,FS_Error,"Storage*, const char*, const char*" | ||||
| Function,+,storage_common_mkdir,FS_Error,"Storage*, const char*" | ||||
| Function,+,storage_common_remove,FS_Error,"Storage*, const char*" | ||||
| Function,+,storage_common_rename,FS_Error,"Storage*, const char*, const char*" | ||||
| Function,+,storage_common_resolve_path_and_ensure_app_directory,void,"Storage*, FuriString*" | ||||
| Function,+,storage_common_stat,FS_Error,"Storage*, const char*, FileInfo*" | ||||
| Function,+,storage_common_timestamp,FS_Error,"Storage*, const char*, uint32_t*" | ||||
| Function,+,storage_dir_close,_Bool,File* | ||||
| Function,+,storage_dir_exists,_Bool,"Storage*, const char*" | ||||
| Function,+,storage_dir_open,_Bool,"File*, const char*" | ||||
| Function,+,storage_dir_read,_Bool,"File*, FileInfo*, char*, uint16_t" | ||||
| Function,-,storage_dir_rewind,_Bool,File* | ||||
|  | ||||
| 
 | 
| @ -35,6 +35,8 @@ struct FuriThread { | ||||
|     void* state_context; | ||||
| 
 | ||||
|     char* name; | ||||
|     char* appid; | ||||
| 
 | ||||
|     configSTACK_DEPTH_TYPE stack_size; | ||||
|     FuriThreadPriority priority; | ||||
| 
 | ||||
| @ -122,11 +124,25 @@ FuriThread* furi_thread_alloc() { | ||||
|     thread->output.buffer = furi_string_alloc(); | ||||
|     thread->is_service = false; | ||||
| 
 | ||||
|     FuriThread* parent = NULL; | ||||
|     if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { | ||||
|         // TLS is not available, if we called not from thread context
 | ||||
|         parent = pvTaskGetThreadLocalStoragePointer(NULL, 0); | ||||
| 
 | ||||
|         if(parent && parent->appid) { | ||||
|             furi_thread_set_appid(thread, parent->appid); | ||||
|         } else { | ||||
|             furi_thread_set_appid(thread, "unknown"); | ||||
|         } | ||||
|     } else { | ||||
|         // if scheduler is not started, we are starting driver thread
 | ||||
|         furi_thread_set_appid(thread, "driver"); | ||||
|     } | ||||
| 
 | ||||
|     FuriHalRtcHeapTrackMode mode = furi_hal_rtc_get_heap_track_mode(); | ||||
|     if(mode == FuriHalRtcHeapTrackModeAll) { | ||||
|         thread->heap_trace_enabled = true; | ||||
|     } else if(mode == FuriHalRtcHeapTrackModeTree && furi_thread_get_current_id()) { | ||||
|         FuriThread* parent = pvTaskGetThreadLocalStoragePointer(NULL, 0); | ||||
|         if(parent) thread->heap_trace_enabled = parent->heap_trace_enabled; | ||||
|     } else { | ||||
|         thread->heap_trace_enabled = false; | ||||
| @ -153,6 +169,7 @@ void furi_thread_free(FuriThread* thread) { | ||||
|     furi_assert(thread->state == FuriThreadStateStopped); | ||||
| 
 | ||||
|     if(thread->name) free((void*)thread->name); | ||||
|     if(thread->appid) free((void*)thread->appid); | ||||
|     furi_string_free(thread->output.buffer); | ||||
| 
 | ||||
|     free(thread); | ||||
| @ -165,6 +182,13 @@ void furi_thread_set_name(FuriThread* thread, const char* name) { | ||||
|     thread->name = name ? strdup(name) : NULL; | ||||
| } | ||||
| 
 | ||||
| void furi_thread_set_appid(FuriThread* thread, const char* appid) { | ||||
|     furi_assert(thread); | ||||
|     furi_assert(thread->state == FuriThreadStateStopped); | ||||
|     if(thread->appid) free((void*)thread->appid); | ||||
|     thread->appid = appid ? strdup(appid) : NULL; | ||||
| } | ||||
| 
 | ||||
| void furi_thread_mark_as_service(FuriThread* thread) { | ||||
|     thread->is_service = true; | ||||
| } | ||||
| @ -498,6 +522,20 @@ const char* furi_thread_get_name(FuriThreadId thread_id) { | ||||
|     return (name); | ||||
| } | ||||
| 
 | ||||
| const char* furi_thread_get_appid(FuriThreadId thread_id) { | ||||
|     TaskHandle_t hTask = (TaskHandle_t)thread_id; | ||||
|     const char* appid = "system"; | ||||
| 
 | ||||
|     if(!FURI_IS_IRQ_MODE() && (hTask != NULL)) { | ||||
|         FuriThread* thread = (FuriThread*)pvTaskGetThreadLocalStoragePointer(hTask, 0); | ||||
|         if(thread) { | ||||
|             appid = thread->appid; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return (appid); | ||||
| } | ||||
| 
 | ||||
| uint32_t furi_thread_get_stack_space(FuriThreadId thread_id) { | ||||
|     TaskHandle_t hTask = (TaskHandle_t)thread_id; | ||||
|     uint32_t sz; | ||||
|  | ||||
| @ -87,6 +87,16 @@ void furi_thread_free(FuriThread* thread); | ||||
|  */ | ||||
| void furi_thread_set_name(FuriThread* thread, const char* name); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Set FuriThread appid | ||||
|  * Technically, it is like a "process id", but it is not a system-wide unique identifier. | ||||
|  * All threads spawned by the same app will have the same appid. | ||||
|  *  | ||||
|  * @param thread  | ||||
|  * @param appid  | ||||
|  */ | ||||
| void furi_thread_set_appid(FuriThread* thread, const char* appid); | ||||
| 
 | ||||
| /** Mark thread as service
 | ||||
|  * The service cannot be stopped or removed, and cannot exit from the thread body | ||||
|  *  | ||||
| @ -233,10 +243,37 @@ uint32_t furi_thread_flags_get(void); | ||||
| 
 | ||||
| uint32_t furi_thread_flags_wait(uint32_t flags, uint32_t options, uint32_t timeout); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Enumerate threads | ||||
|  *  | ||||
|  * @param thread_array array of FuriThreadId, where thread ids will be stored | ||||
|  * @param array_items array size | ||||
|  * @return uint32_t threads count | ||||
|  */ | ||||
| uint32_t furi_thread_enumerate(FuriThreadId* thread_array, uint32_t array_items); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get thread name | ||||
|  *  | ||||
|  * @param thread_id  | ||||
|  * @return const char* name or NULL | ||||
|  */ | ||||
| const char* furi_thread_get_name(FuriThreadId thread_id); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get thread appid | ||||
|  *  | ||||
|  * @param thread_id  | ||||
|  * @return const char* appid | ||||
|  */ | ||||
| const char* furi_thread_get_appid(FuriThreadId thread_id); | ||||
| 
 | ||||
| /**
 | ||||
|  * @brief Get thread stack watermark | ||||
|  *  | ||||
|  * @param thread_id  | ||||
|  * @return uint32_t  | ||||
|  */ | ||||
| uint32_t furi_thread_get_stack_space(FuriThreadId thread_id); | ||||
| 
 | ||||
| /** Get STDOUT callback for thead
 | ||||
|  | ||||
| @ -41,6 +41,7 @@ void flipper_init() { | ||||
|             FLIPPER_SERVICES[i].app, | ||||
|             NULL); | ||||
|         furi_thread_mark_as_service(thread); | ||||
|         furi_thread_set_appid(thread, FLIPPER_SERVICES[i].appid); | ||||
| 
 | ||||
|         furi_thread_start(thread); | ||||
|     } | ||||
|  | ||||
| @ -83,7 +83,7 @@ static DirWalkResult | ||||
|                 end = true; | ||||
|             } | ||||
| 
 | ||||
|             if((info.flags & FSF_DIRECTORY) && dir_walk->recursive) { | ||||
|             if(file_info_is_dir(&info) && dir_walk->recursive) { | ||||
|                 // step into
 | ||||
|                 DirIndexList_push_back(dir_walk->index_list, dir_walk->current_index); | ||||
|                 dir_walk->current_index = 0; | ||||
|  | ||||
| @ -344,7 +344,7 @@ bool tar_archive_add_dir(TarArchive* archive, const char* fs_full_path, const ch | ||||
|                 furi_string_set(element_name, name); | ||||
|             } | ||||
| 
 | ||||
|             if(file_info.flags & FSF_DIRECTORY) { | ||||
|             if(file_info_is_dir(&file_info)) { | ||||
|                 success = | ||||
|                     tar_archive_dir_add_element(archive, furi_string_get_cstr(element_name)) && | ||||
|                     tar_archive_add_dir( | ||||
|  | ||||
| @ -321,6 +321,7 @@ class ApplicationsCGenerator: | ||||
|         return f""" | ||||
|     {{.app = {app.entry_point}, | ||||
|      .name = "{app.name}", | ||||
|      .appid = "{app.appid}",  | ||||
|      .stack_size = {app.stack_size}, | ||||
|      .icon = {f"&{app.icon}" if app.icon else "NULL"}, | ||||
|      .flags = {'|'.join(f"FlipperApplicationFlag{flag}" for flag in app.flags)} }}""" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Sergey Gavrilov
						Sergey Gavrilov